“Vladik Khononov 是一位独特的思想家,多年来一直在应用 DDD 解决实际业务问题。他的想法不断推动整个 DDD 社区向前发展,而这本书将激励刚开始的 DDD 从业者。”
Nick Tune,技术顾问
“Vladik Khononov is a unique thinker who has been applying DDD to solve real business problems for years. His ideas constantly move the whole DDD community forward, and this book will inspire beginning DDD practitioners.”
Nick Tune, Technology Consultant
“回顾我对这本书草稿的阅读,想到的是它实现了它的标题!这是一本引人入胜且内容丰富的实践指南,涵盖了从战略到技术设计的 DDD 范围。我在我有经验的领域获得了新的见解和理解,并填补了我很少接触的概念和实践。弗拉德是一位很棒的老师!”
Bredemeyer Consulting建筑顾问 Ruth Malan
“Reflecting on my readings of drafts of this book, the thing that comes to mind, with a great deal of joy at the thought, is that it delivers on its title! It is an inviting and informative practice guide, covering the scope of DDD from strategy to technical design. I’ve gained new insight and understanding in areas where I have experience and filled in concepts and practices I’d had less exposure to. Vlad is a wonderful teacher!”
Ruth Malan, Architecture Consultant at Bredemeyer Consulting
“作为 DDD 从业者,Vlad 拥有很多来之不易的经验,从事过一些非常复杂的项目,并且乐于分享这些知识。在这本书中,他以独特的方式讲述了 DDD 的故事,为学习提供了一个很好的视角。本书面向新手,但作为长期的 DDD 从业者,同时撰写和谈论 DDD,我发现我从他的角度学到了很多东西。”
Julie Lerman,软件教练、O'Reilly 作者和连续 DDD 倡导者
“Vlad has a lot of hard-won experience as a DDD practitioner working on some deeply complex projects and has been generous in sharing that knowledge. In this book, he tells the story of DDD in a unique way providing a great perspective for learning. This book is aimed at newcomers, yet as a longtime DDD practitioner who also writes and speaks about DDD, I found that I learned so much from his perspective.”
Julie Lerman, Software Coach, O’Reilly Author, and Serial DDD Advocate
对齐软件架构和业务战略
Aligning Software Architecture and Business Strategy
版权所有 © 2022 Vladislav Khononov。版权所有。
Copyright © 2022 Vladislav Khononov. All rights reserved.
在美利坚合众国印刷。
Printed in the United States of America.
由O'Reilly Media, Inc. 出版 ,1005 Gravenstein Highway North, Sebastopol, CA 95472。
Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
购买 O'Reilly 书籍可用于教育、商业或促销用途。大多数书籍还提供在线版本 ( http://oreilly.com )。如需更多信息,请联系我们的企业/机构销售部:800-998-9938 或 corporate@oreilly.com。
O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com.
有关发布详细信息,请参阅 http://oreilly.com/catalog/errata.csp?isbn=9781098100131 。
See http://oreilly.com/catalog/errata.csp?isbn=9781098100131 for release details.
O'Reilly 徽标是 O'Reilly Media, Inc. 的注册商标。 Learning Domain-Driven Design、封面图片和相关商业外观是 O'Reilly Media, Inc. 的商标。
The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Learning Domain-Driven Design, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.
本作品所表达的观点是作者的观点,不代表出版者的观点。尽管出版商和作者已尽善意努力确保本作品中包含的信息和说明准确无误,但出版商和作者不对错误或遗漏承担任何责任,包括但不限于对因使用或对这项工作的依赖。使用本作品中包含的信息和说明的风险由您自行承担。如果本作品包含或描述的任何代码样本或其他技术受开源许可或他人知识产权的约束,您有责任确保您对它们的使用符合此类许可和/或权利。
The views expressed in this work are those of the author, and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.
978-1-098-10013-1
978-1-098-10013-1
[大规模集成电路]
[LSI]
领域驱动设计为从业务的角度构建软件的协作方法——即领域及其目标问题。它是最初由埃里克·埃文斯 (Eric Evans) 于 2003 年在 DDD 社区中被亲切地称为“蓝皮书”的出版物中创造出来。书名是领域驱动设计:解决软件核心的复杂性问题。
Domain-driven design provides a set of practices for a collaborative approach to building software from the perspective of the business—that is, the domain, and its problems that you are targeting. It was originally coined by Eric Evans in 2003 with the publication of what is fondly known in the DDD community as “The Blue Book.” The book’s title is Domain-Driven Design: Tackling Complexity in the Heart of Software.
虽然解决复杂性并提供通向清晰的路径是领域驱动设计的目标,但有许多伟大的想法可以应用于甚至不那么复杂的软件项目。DDD 提醒我们,软件开发人员并不是唯一参与构建软件的人。正在为其构建软件的领域专家对正在解决的问题具有批判性的理解。当我们首先应用“战略设计”来理解业务问题(即领域)并将问题分解为更小的、可解决的、相互关联的问题时,我们在整个创建阶段建立了伙伴关系。与领域专家的合作也促使我们使用领域语言进行交流,而不是强迫业务方面的人员学习软件的技术语言。
While tackling complexity and providing a path to clarity is the goal of domain-driven design, there are so many great ideas that can be applied to even less complicated software projects. DDD reminds us that software developers are not the only people involved in building software. The domain experts, for whom the software is being built, bring critical understanding of the problems being solved. We create a partnership throughout the stages of creation as we first apply “strategic design” to understand the business problem, a.k.a. the domain, and break the problem down into smaller, solvable, interconnected problems. The partnership with the domain experts also drives us to communicate in the language of the domain, rather than forcing those on the business side to learn the technical language of software.
基于 DDD 的项目的第二阶段是“战术设计”,我们将战略设计的发现转化为软件架构和实施。同样,DDD 为组织这些领域和避免进一步的复杂性提供了指导和模式。战术设计继续与领域专家合作,即使他们查看软件团队构建的代码,他们也会认出他们的领域语言。
The second stage of a DDD-based project is “tactical design,” where we transform the discoveries of strategic design into software architecture and implementation. Again, DDD provides guidance and patterns for organizing these domains and avoiding further complexity. Tactical design continues the partnership with the domain experts who will recognize their domain language even as they look at the code built by the software teams.
自“蓝皮书”出版以来的这些年里,不仅许多组织从这些想法中受益,而且还形成了一个由经验丰富的 DDD 实践者组成的社区。DDD 的协作性质导致该社区分享他们的经验和观点,并创建工具来帮助团队接受这些想法并从中受益。在 2019 年 Explore DDD 的主题演讲中,Eric Evans 鼓励社区继续发展 DDD——不仅是它的实践,而且是寻找更有效地分享其想法的方法。
Over the years since the publication of “The Blue Book,” not only have many organizations benefited from the ideas, but a community of experienced DDD practitioners has evolved. And the collaborative nature of DDD has resulted in this community sharing their experiences and perspective and creating tools to help teams embrace and benefit from these ideas. In a keynote at Explore DDD in 2019, Eric Evans encouraged the community to continue to evolve DDD—not only its practices but in finding ways to more effectively share its ideas.
这让我想到了为什么我如此喜欢学习领域驱动设计。通过他的会议演讲和其他著作,我已经成为 Vlad 的粉丝。作为 DDD 从业者,他在一些非常复杂的项目中工作,积累了很多来之不易的经验,并且乐于分享这些知识。在这本书中,他以独特的方式讲述了 DDD 的“故事”(不是它的历史,而是它的概念),为学习提供了一个很好的视角。本书面向新手,但作为长期的 DDD 从业者,同时也撰写和谈论 DDD,我发现我从他的角度学到了很多东西。在这本书出版之前,我很想在我关于 Pluralsight 的 DDD 基础课程中参考他的书,并且已经在与客户的对话中分享了一些这种观点。
And this brings me to why I am such a fan of Learning Domain-Driven Design. I was already a fan of Vlad through his conference speaking and other writings. He has a lot of hard-won experience as a DDD practitioner working on some deeply complex projects and has been generous in sharing that knowledge. In this book, he tells the “story” of DDD (not its history, but its concepts) in a unique way, providing a great perspective for learning. This book is aimed at newcomers, yet as a longtime DDD practitioner who also writes and speaks about DDD, I found that I learned so much from his perspective. I was eager to reference his book in my DDD Fundamentals course on Pluralsight before the book was even published and have already been sharing some of this perspective in conversations with clients.
DDD 入门可能会令人困惑。正如我们使用 DDD 来降低项目的复杂性一样,Vlad 以一种降低主题本身复杂性的方式呈现 DDD。他所做的不仅仅是解释 DDD 的原则。本书的后半部分分享了一些从 DDD 演变而来的重要实践,例如 EventStorming,解决了业务重点或组织演变的问题以及这可能如何影响软件,并讨论了 DDD 如何与微服务保持一致以及如何集成它与一系列众所周知的软件模式。我认为《Learning Domain-Driven Design》对于新手来说是 DDD 的极好介绍,对于有经验的从业者也非常值得一读。
Getting started with DDD can be confusing. Just as we use DDD to reduce the complexity of projects, Vlad presents DDD in a way that reduces the complexity of the topic itself. And he does more than explain the principles of DDD. The latter portion of the book shares some important practices that have evolved from DDD, such as EventStorming, addresses the problem of evolving the business focus or organization and how this might affect the software, and discusses how DDD aligns with microservices and how you can integrate it with a slew of well-known software patterns. I think Learning Domain-Driven Design will be an excellent introduction to DDD for newcomers, and a very worthy read for experienced practitioners as well.
我清楚地记得我开始第一份真正的软件工程工作的那一天。我既欣喜若狂又害怕。在高中时期为本地企业开发软件后,我渴望成为一名“真正的程序员”并为该国最大的外包公司之一编写一些代码。
I vividly remember the day I started my first real software engineering job. I was both ecstatic and terrified. After hacking software for local businesses during my high school years, I was eager to become a “real programmer” and write some code for one of the country’s largest outsourcing companies.
在那里的第一天,我的新同事向我展示了诀窍。在设置了公司电子邮件并通过时间跟踪系统后,我们终于转向了有趣的东西:公司的编码风格和标准。有人告诉我“在这里,我们总是编写设计良好的代码并使用分层架构。” 我们详细介绍了三层(数据访问层、业务逻辑层和表示层)中每一层的定义,然后讨论了满足这些层需求的技术和框架。当时,公认的存储数据的解决方案是 Microsoft SQL Server 2000,它是在数据访问层中使用 ADO.NET 集成的。表示层震撼了用于桌面应用程序的 WinForms 或用于 web 的 ASP.NET WebForms。我们在这两层上花了很多时间,所以当业务逻辑层没有得到任何关注时,我感到很困惑:
In my first days there, my new colleagues were showing me the ropes. After setting up the corporate email and going through the time-tracking system, we finally moved on to the interesting stuff: the company’s coding style and standards. I was told that “here, we always write well-designed code and use the layered architecture.” We went through the definition of each of the three layers—the data access, business logic, and presentation layers—and then discussed the technologies and frameworks for addressing the layers’ needs. Back then, the accepted solution for storing data was Microsoft SQL Server 2000, and it was integrated using ADO.NET in the data access layer. The presentation layer rocked either WinForms for desktop applications or ASP.NET WebForms for the web. We spent quite some time on these two layers, so I was puzzled when the business logic layer didn’t get any attention:
“但是业务逻辑层呢?”
“那个很简单。这是您实现业务逻辑的地方。”
“但什么是业务逻辑?”
“哦,业务逻辑是实现需求所需的所有循环和‘if-else’语句。”
“But what about the business logic layer?”
“That one is straightforward. Here is where you implement the business logic.”
“But what is business logic?”
“Oh, business logic is all the loops and ‘if-else’ statements you need in order to implement the requirements.”
那一天,我开始了我的旅程,以找出业务逻辑到底是什么,以及它到底应该如何在设计良好的代码中实现。我花了三年多的时间才终于找到答案。
That day I began my journey to find out what exactly business logic is and how on earth it should be implemented in well-designed code. It took me more than three years to finally find the answer.
答案在埃里克埃文斯的开创性著作中, 领域驱动设计:解决软件核心的复杂性。事实证明我没有看错。业务逻辑确实很重要:它是软件的心脏!然而不幸的是,我又花了三年时间才理解 Eric 分享的智慧。这本书非常先进,英语是我的第三语言这一事实对我没有帮助。
The answer was in Eric Evans’s seminal book, Domain-Driven Design: Tackling Complexity in the Heart of Software. It turned out that I wasn’t wrong. Business logic is indeed important: it is the heart of software! Unfortunately, however, it took me another three years to understand the wisdom Eric shared. The book is very advanced, and the fact that English is my third language didn’t help.
不过,最终一切都水到渠成,我接受了领域驱动设计 (DDD) 方法。我学习了 DDD 的原则和模式、建模和实现业务逻辑的复杂性,以及如何解决我正在构建的软件核心的复杂性。尽管有障碍,但这绝对是值得的。进入领域驱动设计对我来说是一次改变职业生涯的经历。
Eventually, though, everything fell into place, and I made peace with the domain-driven design (DDD) methodology. I learned the principles and patterns of DDD, the intricacies of modeling and implementing the business logic, and how to tackle the complexity in the heart of the software that I was building. Despite the obstacles, it definitely was worth it. Getting into domain-driven design was a career-changing experience for me.
在过去的 10 年里,我向不同公司的同事介绍了领域驱动设计,进行了面对面的课程,并教授了在线课程。教学视角不仅帮助我加深了知识,也让我优化了解释领域驱动设计的原则和模式的方式。
Over the past 10 years, I have introduced domain-driven design to my colleagues at different companies, conducted in-person classes, and taught online courses. The teaching perspective not only helped me deepen my knowledge, but also allowed me to optimize the way I explain the principles and patterns of domain-driven design.
正如经常发生的那样,教学比学习更具挑战性。我非常喜欢 Eliyahu M. Goldratt 的作品和教学。Eliyahu 曾经说过,从正确的角度来看,即使是最复杂的系统本质上也很简单。在我教授 DDD 的这些年里,我一直在寻找一种可以揭示领域驱动设计内在简单性的方法模型。
As often happens, teaching is even more challenging than learning. I’m a huge fan of Eliyahu M. Goldratt’s work and teachings. Eliyahu used to say that even the most complex systems are inherently simple when viewed from the right angle. During my years of teaching DDD, I was looking for a model of the methodology that would uncover the inherent simplicity of domain-driven design.
这本书是我努力的结果。它的目标是使领域驱动设计民主化;使其更易于理解和更易于使用。我相信 DDD 方法论绝对是无价的,尤其是在设计现代软件系统时。本书将为您提供足够的工具,让您在日常工作中开始应用领域驱动设计。
This book is the result of my efforts. Its goal is to democratize domain-driven design; make it easier to understand and more accessible to employ. I believe that the DDD methodology is absolutely invaluable, especially when designing modern software systems. This book will give you just enough tools to start applying domain-driven design in your day-to-day work.
我相信领域驱动设计原则和模式的知识对所有级别的软件工程师都有用:初级、高级、职员和负责人。DDD 不仅提供了用于建模和有效实现软件的工具和技术,它还阐明了软件工程中经常被忽视的一个方面:上下文。了解系统的业务问题后,您会更有效地选择合适的解决方案。一个没有设计不足或过度设计的解决方案,但可以满足业务需求和目标。
I believe that knowledge of domain-driven design principles and patterns will be useful for software engineers at all levels: junior, senior, staff, and principal. Not only does DDD provide tools and techniques for modeling and effectively implementing software, it also illuminates an often-overlooked aspect of software engineering: the context. Equipped with the knowledge of the system’s business problem, you will be much more effective at choosing the appropriate solution. A solution that is not under- or over-engineered, but addresses business needs and goals.
领域驱动设计对软件更为重要架构师,对于有抱负的软件架构师更是如此。它的战略设计决策工具将帮助您将大型系统分解为组件(服务、微服务或子系统),并设计组件如何相互集成以形成一个系统。
Domain-driven design is even more important for software architects, and even more so for aspiring software architects. Its strategic design decision tools will help you decompose a large system into components—services, microservices, or subsystems—and design how the components are integrated with one another to form a system.
最后,在本书中,我们不仅将讨论如何设计软件,还将讨论如何随着业务环境的变化共同改进设计。软件工程的这一关键方面将帮助您随着时间的推移保持系统设计的“形状”,并防止其退化为一个大泥球。
Ultimately, in this book we will discuss not only how to design software, but also how to co-evolve the design with changes in its business context. That crucial aspect of software engineering will help you keep the system’s design “in shape” over time and prevent its degradation into a big ball of mud.
WolfDesk 提供服务台工单管理系统作为服务。如果您的初创公司需要为您的客户提供支持,使用 WolfDesk 的解决方案,您可以立即启动并运行。
WolfDesk provides a help desk tickets management system as a service. If your start-up company needs to provide support to your customers, with WolfDesk’s solution you can get up and running in no time.
WolfDesk 使用与其竞争对手不同的支付模式。它不是向每个用户收费,而是允许租户根据需要设置尽可能多的用户,并且租户根据每个收费周期打开的支持工单数量收费。不设最低手续费,月票一定门槛有自动量减:每月开500张以上10%,开750张以上20%,每月开1000张以上30%。
WolfDesk uses a different payment model than its competitors. Instead of charging a fee per user, it allows the tenants to set up as many users as needed, and the tenants are charged for the number of support tickets opened per charging period. There is no minimum fee, and there are automatic volume discounts for certain thresholds of monthly tickets: 10% for opening more than 500 tickets, 20% for opening more than 750 tickets, and 30% for opening more than 1,000 tickets per month.
为防止租户滥用业务模型,WolfDesk 的工单生命周期算法可确保自动关闭非活动工单,鼓励客户在需要进一步支持时打开新工单。此外,WolfDesk 实施了一个欺诈检测系统,该系统分析消息并检测在同一张票中讨论不相关主题的情况。
To prevent tenants from abusing the business model, WolfDesk’s ticket lifecycle algorithm ensures that inactive tickets are closed automatically, encouraging customers to open new tickets when further support is needed. Moreover, WolfDesk implements a fraud detection system that analyzes messages and detects cases of unrelated topics being discussed in the same ticket.
为了帮助其租户简化与支持相关的工作,WolfDesk 实施了“支持自动驾驶”功能。自动驾驶仪会分析新的工单,并尝试从租户的工单历史记录中自动找到匹配的解决方案。该功能允许进一步缩短工单的使用寿命,鼓励客户打开新工单以解决更多问题。
To help its tenants streamline the support-related work, WolfDesk has implemented a “support autopilot” feature. The autopilot analyzes new tickets and tries to automatically find a matching solution from the tenant’s ticket history. The functionality allows for further reducing the tickets’ lifespans, encouraging customers to open new tickets for further questions.
WolfDesk 结合了所有安全标准和措施来验证和授权其租户的用户,还允许租户使用他们现有的用户管理系统配置单点登录 (SSO)。
WolfDesk incorporates all the security standards and measures to authenticate and authorize its tenants’ users and also allows tenants to configure a single sign-on (SSO) with their existing user management systems.
管理界面允许租户配置工单类别的可能值,以及它支持的租户产品列表。
The administration interface allows tenants to configure the possible values for the tickets’ categories, as well as a list of the tenant’s products that it supports.
为了能够仅在租户的工作时间内将新工单发送给租户的支持代理,WolfDesk 允许输入每个代理的轮班时间表。
To be able to route new tickets to the tenant’s support agents only during their working hours, WolfDesk allows the entry of each agent’s shift schedule.
由于 WolfDesk 提供的服务不收取最低费用,因此它必须优化其基础设施,以最大限度地降低新租户的入职成本。为此,WolfDesk 利用无服务器计算,使其能够根据活动票证上的操作弹性扩展其计算资源。
Since WolfDesk provides its service with no minimal fee, it has to optimize its infrastructure in a way that minimizes the costs of onboarding a new tenant. To do that, WolfDesk leverages serverless computing, which allows it to elastically scale its compute resources based on the operations on active tickets.
本书使用以下排版约定:
The following typographical conventions are used in this book:
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
Indicates new terms, URLs, email addresses, filenames, and file extensions.
Constant widthConstant width用于程序列表,以及在段落中引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords.
该元素表示一般注释。
This element signifies a general note.
补充材料(代码示例、练习等)可供下载在https://learning-ddd.com。
Supplemental material (code examples, exercises, etc.) is available for download at https://learning-ddd.com.
书中提供的所有代码示例都是用 C# 语言实现的。通常,您在章节中看到的代码示例是演示所讨论概念的摘录。
All the code samples presented in the book are implemented in the C# language. Generally, the code samples you see in the chapters are excerpts demonstrating the discussed concepts.
当然,本书中讨论的概念和技术不仅限于 C# 语言或面向对象的编程方法。一切都与其他语言和其他编程范例相关。因此,请随意用您喜欢的语言实现本书的示例并与我分享。我很乐意将它们添加到本书的网站上。
Of course, the concepts and techniques discussed in the book are not limited to the C# language or to the object-oriented programming approach. Everything is relevant for other languages and other programming paradigms. As a result, feel free to implement the book’s samples in your favorite language and share them with me. I’ll be happy to add them to the book’s website.
如果您在使用代码示例时遇到技术问题或问题,请发送电子邮件至bookquestions@oreilly.com。
If you have a technical question or a problem using the code examples, please email bookquestions@oreilly.com.
本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您要复制代码的重要部分,否则无需联系我们获得许可。例如,编写一个使用本书中几段代码的程序不需要许可。销售或分发 O'Reilly 图书中的示例需要获得许可。通过引用本书和引用示例代码来回答问题不需要许可。将本书中的大量示例代码合并到您的产品文档中确实需要获得许可。
This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission.
我们赞赏但通常不需要署名。署名通常包括书名、作者、出版商和 ISBN。例如:“ Vlad Khononov (O'Reilly) 的《学习领域驱动设计》 。版权所有 2022 Vladislav Khononov,978-1-098-10013-1。”
We appreciate, but generally do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “Learning Domain-Driven Design by Vlad Khononov (O’Reilly). Copyright 2022 Vladislav Khononov, 978-1-098-10013-1.”
如果您觉得您对代码示例的使用不属于合理使用或上述许可范围,请随时通过permissions@oreilly.com与我们联系。
If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com.
40 多年来,O'Reilly Media提供技术和业务培训、知识和洞察力来帮助公司取得成功。
For more than 40 years, O’Reilly Media has provided technology and business training, knowledge, and insight to help companies succeed.
我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专长。O'Reilly 的在线学习平台让您可以按需访问实时培训课程、深度学习路径、交互式编码环境,以及来自 O'Reilly 和 200 多家其他出版商的大量文本和视频。如需更多信息,请访问http://oreilly.com。
Our unique network of experts and innovators share their knowledge and expertise through books, articles, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O’Reilly and 200+ other publishers. For more information, visit http://oreilly.com.
请将有关本书的评论和问题发送给出版商:
Please address comments and questions concerning this book to the publisher:
我们有本书的网页,其中列出了勘误表、示例和任何其他信息。您可以通过https://oreil.ly/lddd访问此页面。
We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at https://oreil.ly/lddd.
发送电子邮件至bookquestions@oreilly.com发表评论或询问有关本书的技术问题。
Email bookquestions@oreilly.com to comment or ask technical questions about this book.
有关我们的书籍和课程的新闻和信息,请访问http://oreilly.com。
For news and information about our books and courses, visit http://oreilly.com.
在 Facebook 上找到我们: http: //facebook.com/oreilly
Find us on Facebook: http://facebook.com/oreilly
在 Twitter 上关注我们: http: //twitter.com/oreillymedia
Follow us on Twitter: http://twitter.com/oreillymedia
在 YouTube 上观看我们: http: //youtube.com/oreillymedia
Watch us on YouTube: http://youtube.com/oreillymedia
最初,这本书的标题是“什么是领域驱动设计?” 并于 2019 年作为报告发布。如果没有报告,学习领域驱动设计就不会问世,我不得不感谢那些制作“什么是领域驱动设计?”的人。可能:Chris Guzikowski、Ryan Shaw 和 Alicia Young。1个
Originally, this book was titled “What Is Domain-Driven Design?” and was published as a report in 2019. Learning Domain-Driven Design would not have seen the light of day without the report, and I’m obliged to thank those who made “What Is Domain-Driven Design?” possible: Chris Guzikowski, Ryan Shaw, and Alicia Young.1
如果没有 O'Reilly 的内容总监和多元化人才主管 Melissa Duffield,这本书也不会成为可能,她支持这个项目并使它成为现实。梅丽莎,谢谢你的帮助!
This book also wouldn’t have been possible without O’Reilly’s Content Director and Diversity Talent Lead, Melissa Duffield, who championed the project and made it happen. Thank you, Melissa, for all your help!
吉尔·伦纳德 (Jill Leonard) 是本书的开发编辑、项目经理和总教练。吉尔在这项工作中的作用怎么强调都不为过。吉尔,非常感谢你的辛勤工作和帮助!特别感谢你让我保持动力,即使我考虑改名并躲在国外。
Jill Leonard was the book’s development editor, project manager, and head coach. Jill’s role in this work cannot be overstated. Jill, thank you so much for all your hard work and help! Extra thanks for keeping me motivated, even when I considered changing my name and hiding in a foreign country.
非常感谢使本书不仅可写而且可读的制作团队:Kristen Brown、Audrey Doyle、Kate Dullea、Robert Romano 和 Katherine Tozer。就此而言,我要感谢整个 O'Reilly 团队所做的出色工作。和你一起工作真是梦想成真!
A huge thanks to the production team for making the book not only writable but readable: Kristen Brown, Audrey Doyle, Kate Dullea, Robert Romano, and Katherine Tozer. For that matter, I want to thank the whole O’Reilly team for the great work you do. It’s a dream come true to be working with you!
感谢我采访和咨询过的所有人:Zsofia Herendi、Scott Hirleman、Trond Hjorteland、Mark Lisker、Chris Richardson、Vaughn Vernon 和 Ivan Zakrevsky。感谢您的智慧,感谢您在我需要帮助时出现!
Thanks to all the people I interviewed and consulted with: Zsofia Herendi, Scott Hirleman, Trond Hjorteland, Mark Lisker, Chris Richardson, Vaughn Vernon, and Ivan Zakrevsky. Thank you for your wisdom and for being there when I needed help!
特别感谢阅读初稿并帮助我完成最终书的审稿团队:Julie Lerman、Ruth Malan、Diana Montalion、Andrew Padilla、Rodion Promyshlennikov、Viktor Pshenitsyn、Alexei Torunov、Nick Tune、Vasiliy Vasilyuk 和 Rebecca Wirfs-Brock。您的支持、反馈和批评帮助极大。谢谢你!
Special thanks to the team of reviewers who read through the early drafts and helped me shape the final book: Julie Lerman, Ruth Malan, Diana Montalion, Andrew Padilla, Rodion Promyshlennikov, Viktor Pshenitsyn, Alexei Torunov, Nick Tune, Vasiliy Vasilyuk, and Rebecca Wirfs-Brock. Your support, feedback, and critique helped immensely. Thank you!
我还要感谢 Kenny Baas-Schwegler、Alberto Brandolini、Eric Evans、Marco Heimeshoff、Paul Rayner、Mathias Verraes 以及令人惊叹的领域驱动设计社区的其他成员。你知道你是谁。你是我的老师和导师。感谢您在社交媒体、博客和会议上分享您的知识!
I also want to thank Kenny Baas-Schwegler, Alberto Brandolini, Eric Evans, Marco Heimeshoff, Paul Rayner, Mathias Verraes, and the rest of the amazing domain-driven design community. You know who you are. You are my teachers and mentors. Thank you for sharing your knowledge on social media, blogs, and conferences!
我非常感谢我亲爱的妻子维拉,她一直支持我进行疯狂的项目,并试图保护我免受可能会分散我写作注意力的事情的影响。我保证最终整理地下室。它很快就会发生!
I’m most indebted to my dear wife, Vera, for always supporting me in my crazy projects and trying to guard me from things that could distract me from writing. I promise to finally declutter the basement. It is going to happen soon!
最后,我想将本书献给我们挚爱的 Galina Ivanovna Tyumentseva,她在这个项目中给予了我如此多的支持,而我们在本书的写作过程中遗憾地失去了她。我们会永远记住你。
Finally, I want to dedicate this book to our beloved Galina Ivanovna Tyumentseva, who supported me so much in this project and whom we sadly lost during the writing of this book. We will always remember you.
#AdoptDontShop
#AdoptDontShop
软件工程很难。要想在这方面取得成功,我们必须不断学习,无论是尝试新语言、探索新技术,还是跟上新的流行框架。然而,每周学习一个新的 JavaScript 框架并不是我们工作中最难的方面。理解新的业务领域可能更具挑战性。
Software engineering is hard. To be successful at it, we have to learn continuously, whether it’s trying new languages, exploring new technologies, or keeping up with new popular frameworks. However, learning a new JavaScript framework every week is not the hardest aspect of our job. Making sense of new business domains can be far more challenging.
在我们的整个职业生涯中,我们不得不为各种业务领域开发软件的情况并不少见:金融系统、医疗软件、在线零售商、市场营销等等。从某种意义上说,这就是我们的工作与大多数其他职业的不同之处。在其他领域工作的人在发现软件工程涉及多少学习时常常感到惊讶,尤其是在更换工作场所时。
Throughout our careers, it’s not uncommon for us to have to develop software for a diverse range of business domains: financial systems, medical software, online retailers, marketing, and many others. In a sense, that is what differentiates our job from most other professions. People working in other fields are often surprised when they find out how much learning is involved in software engineering, especially when changing workplaces.
未能掌握业务领域会导致业务软件的实施不理想。不幸的是,这很常见。根据研究,大约 70% 的软件项目没有按时、按预算或按客户的要求交付。换句话说,绝大多数软件项目都失败了。这个问题是如此深刻和广泛,以至于我们甚至给它起了一个词:软件危机。
Failure to grasp the business domain results in suboptimal implementation of the business software. Unfortunately, that’s quite common. According to studies, approximately 70% of software projects are not delivered on time, on budget, or according to the client’s requirements. In other words, the vast majority of software projects fail. This issue is so deep and widespread that we even have a term for it: software crisis.
软件危机一词早在 1968 年就被引入。1人们会认为在这之后的 50 年里情况会有所改善。在那些年里,引入了许多方法、方法和学科来提高软件工程的效率:敏捷宣言、极限编程、测试驱动开发、高级语言、DevOps 等等。不幸的是,事情并没有太大改变。项目仍然经常失败,软件危机仍然存在。
The term software crisis was introduced all the way back in 1968.1 One would assume that things would have improved in the intervening 50 years. During those years, numerous approaches, methodologies, and disciplines were introduced to make software engineering more effective: Agile Manifesto, extreme programming, test-driven development, high-level languages, DevOps, and others. Unfortunately, things didn’t change much. Projects are still failing quite often and the software crisis is still here.
已经进行了许多研究来调查常见项目失败的原因。2个 尽管研究人员无法查明单一原因,但他们的大部分发现都有一个共同的主题:沟通。阻碍项目的沟通问题可以以不同的方式表现出来;例如,不明确的需求、不确定的项目目标或团队之间的工作协调不力。多年来,我们再次尝试通过引入新的沟通机会、流程和媒介来改善团队间和团队内的沟通。不幸的是,我们项目的成功率仍然没有太大变化。
Many studies have been conducted to investigate the reasons for the common project failures.2 Although researchers have not been able to pinpoint a single cause, most of their findings share a common theme: communication. Communication issues thwarting projects can manifest themselves in different ways; for example, unclear requirements, uncertain project goals, or ineffective coordination of effort between teams. Yet again, over the years, we have tried to improve inter- and intrateam communication by introducing new communication opportunities, processes, and mediums. Unfortunately, the success rates of our projects still didn’t change much.
领域驱动设计(DDD)提出攻击根从不同的角度分析软件项目失败的原因。有效沟通是您将在本书中学习的领域驱动设计工具和实践的中心主题。DDD可以分为战略和战术两部分。
Domain-driven design (DDD) proposes to attack the root cause for failed software projects from a different angle. Effective communication is the central theme of the domain-driven design tools and practices you are about to learn in this book. DDD can be divided into two parts: strategic and tactical.
DDD 的战略工具用于分析业务领域和战略,并促进不同利益相关者之间对业务的共同理解。我们还将使用业务领域的这些知识来推动高级设计决策:将系统分解为组件并定义它们的集成模式。
The strategic tools of DDD are used to analyze business domains and strategy, and to foster a shared understanding of the business between the different stakeholders. We will also use this knowledge of the business domain to drive high-level design decisions: decomposing systems into components and defining their integration patterns.
领域驱动设计的战术工具解决了沟通问题的不同方面。DDD 的战术模式使我们能够以反映业务领域、解决其目标并使用业务语言的方式编写代码。
Domain-driven design’s tactical tools address a different aspect of communication issues. DDD’s tactical patterns allow us to write code in a way that reflects the business domain, addresses its goals, and speaks the language of the business.
DDD 的战略和战术模式和实践都使软件设计与其业务领域保持一致。这就是名称的由来:(业务)领域驱动(软件)设计。
Both the strategic and tactical patterns and practices of DDD align software design with its business domain. That’s where the name comes from: (business) domain-driven (software) design.
领域驱动设计无法将新 JavaScript 库的知识直接安装到您的大脑中,就像在《黑客帝国》中那样。但是,它可以减轻理解业务领域的过程并根据业务战略指导设计决策,从而使您成为更高效的软件工程师。正如您将在本书后面的章节中了解到的那样,软件设计与其业务战略之间的联系越紧密,就越容易维护和发展系统以满足未来的业务需求,最终导致更成功的软件项目。
Domain-driven design won’t make it possible to install the knowledge of new JavaScript libraries directly into your brain, like in The Matrix. However, it will make you a more effective software engineer by alleviating the process of making sense of business domains and guiding the design decisions according to the business strategy. As you will learn in the book’s later chapters, the tighter the connection between the software design and its business strategy is, the easier it will be to maintain and evolve the system to meet the future needs of the business, ultimately leading to more successful software projects.
让我们通过探索战略模式和实践来开始我们的 DDD 之旅。
Let’s start our DDD journey by exploring the strategic patterns and practices.
1个“软件工程。” 北约科学委员会主办的会议报告,德国加米施,1968 年 10 月 7 日至 11 日。
1 “Software Engineering.” Report on a conference sponsored by the NATO Science Committee, Garmisch, Germany, October 7–11, 1968.
2个例如,参见 Kaur、Rupinder 和 Jyotsna Sengupta 博士(2013 年),“软件过程模型和软件开发项目失败分析”,https://arxiv.org/ftp/arxiv/papers/1306/1306.1068 。 .pdf _ 另请参见 Sudhakar, Goparaju Purna (2012),“软件项目关键成功因素模型”。企业信息管理杂志 25 (6), 537–558。
2 See, for example, Kaur, Rupinder, and Dr. Jyotsna Sengupta (2013), “Software Process Models and Analysis on Failure of Software Development Projects,” https://arxiv.org/ftp/arxiv/papers/1306/1306.1068.pdf. See also Sudhakar, Goparaju Purna (2012), “A Model of Critical Success Factors for Software Projects.” Journal of Enterprise Information Management 25(6), 537–558.
在我们就问题达成一致之前讨论解决方案是没有意义的,在我们就解决方案达成一致之前讨论实施步骤也是没有意义的。
Efrat Goldratt-Ashlag 1
There is no sense in talking about the solution before we agree on the problem, and no sense talking about the implementation steps before we agree on the solution.
Efrat Goldratt-Ashlag1
领域驱动设计(DDD)方法可分为两个主要部分:战略设计和战术设计。DDD 的战略方面涉及回答“什么?”的问题。和“为什么?”——我们正在构建什么软件以及我们为什么要构建它。战术部分是关于“如何”的——每个组件是如何实现的。
The domain-driven design (DDD) methodology can be divided into two main parts: strategic design and tactical design. The strategic aspect of DDD deals with answering the questions of “what?” and “why?”—what software we are building and why we are building it. The tactical part is all about the “how”—how each component is implemented.
我们将从探索领域驱动的设计模式和战略设计原则开始我们的旅程:
We will begin our journey by exploring domain-driven design patterns and principles of strategic design:
在第 1 章中,您将学习分析一家公司的业务战略:它为消费者提供什么价值以及它如何与业内其他公司竞争。我们将确定更细粒度的业务构建块,评估它们的战略价值,并分析它们如何影响不同的软件设计决策。
In Chapter 1, you will learn to analyze a company’s business strategy: what value it provides to its consumers and how it competes with other companies in the industry. We will identify finer-grained business building blocks, evaluate their strategic value, and analyze how they affect different software design decisions.
第 2 章介绍领域驱动设计理解业务领域的基本实践:通用语言。您将学习如何培养一种无处不在的语言,并使用它来促进所有与项目相关的利益相关者之间的共同理解。
Chapter 2 introduces domain-driven design’s essential practice for gaining an understanding of the business domain: the ubiquitous language. You will learn how to cultivate a ubiquitous language and use it to foster a shared understanding among all project-related stakeholders.
第 3 章讨论另一个领域驱动设计的核心工具:限界上下文模式。您将了解为什么此工具对于培养无处不在的语言至关重要,以及如何使用它来将发现的知识转化为业务领域的模型。最终,我们将利用限界上下文来设计软件系统的粗粒度组件。
Chapter 3 discusses another domain-driven design core tool: the bounded context pattern. You will learn why this tool is essential for cultivating a ubiquitous language and how to use it to transform discovered knowledge into a model of the business domain. Ultimately, we will leverage bounded contexts to design coarse-grained components of the software system.
在第 4 章中,您将了解影响系统组件集成方式的技术和社会约束,以及解决不同情况和限制的集成模式。我们将讨论每种模式如何影响软件开发团队之间的协作以及组件 API 的设计。
本章最后介绍了上下文图:一种绘制系统限界上下文之间通信的图形符号,并提供项目集成和协作环境的鸟瞰图。
In Chapter 4, you will learn technical and social constraints that affect how system components can be integrated, and integration patterns that address different situations and limitations. We will discuss how each pattern influences collaboration among software development teams and the design of the components’ APIs.
The chapter closes by introducing the context map: a graphical notation that plots communication between the system’s bounded contexts and provides a bird’s-eye view of the project’s integration and collaboration landscapes.
如果您和我一样,就会喜欢编写代码:解决复杂的问题,提出优雅的解决方案,并通过精心设计规则、结构和行为来构建全新的世界。我相信这就是您对领域驱动设计 (DDD) 感兴趣的地方:您想在自己的手艺上做得更好。然而,本章与编写代码无关。在本章中,您将学习公司如何运作:它们存在的原因、追求的目标以及实现目标的策略。
If you are anything like me, you love writing code: solving complex problems, coming up with elegant solutions, and constructing whole new worlds by carefully crafting their rules, structures, and behavior. I believe that’s what interested you in domain-driven design (DDD): you want to be better at your craft. This chapter, however, has nothing to do with writing code. In this chapter, you will learn how companies work: why they exist, what goals they are pursuing, and their strategies for achieving their goals.
当我在我的领域驱动设计课程中教授这些材料时,许多学生实际上会问,“我们需要知道这些材料吗?我们是在编写软件,而不是在经营业务。” 他们问题的答案是响亮的“是”。要设计和构建有效的解决方案,您必须了解问题所在。在我们的上下文中,问题是我们必须构建的软件系统。要理解这个问题,您必须理解它存在的背景——组织的业务战略,以及它通过构建软件寻求获得的价值。
When I teach this material in my domain-driven design classes, many students actually ask, “Do we need to know this material? We are writing software, not running businesses.” The answer to their question is a resounding “yes.” To design and build an effective solution, you have to understand the problem. The problem, in our context, is the software system we have to build. To understand the problem, you have to understand the context within which it exists—the organization’s business strategy, and what value it seeks to gain by building the software.
在本章中,您将学习用于分析公司业务领域及其结构的领域驱动设计工具:其核心、支持和通用子领域。该材料是设计软件的基础。在剩下的章节中,您将学习这些概念影响软件设计的不同方式。
In this chapter, you will learn domain-driven design tools for analyzing a company’s business domain and its structure: its core, supporting, and generic subdomains. This material is the groundwork for designing software. In the remaining chapters, you will learn the different ways these concepts affect software design.
业务领域定义了公司的主要活动领域。一般来说,它是公司为客户提供的服务。例如:
A business domain defines a company’s main area of activity. Generally speaking, it’s the service the company provides to its clients. For example:
FedEx 提供快递服务。
FedEx provides courier delivery.
星巴克以其咖啡而闻名。
Starbucks is best known for its coffee.
沃尔玛是最广为人知的零售机构之一。
Walmart is one of the most widely recognized retail establishments.
一家公司可以在多个业务领域开展业务。例如,亚马逊同时提供零售和云计算服务。优步是一家拼车公司,还提供送餐和自行车共享服务。
A company can operate in multiple business domains. For example, Amazon provides both retail and cloud computing services. Uber is a rideshare company that also provides food delivery and bicycle-sharing services.
重要的是要注意,公司可能会经常更改其业务领域。一个典型的例子是诺基亚,它多年来在木材加工、橡胶制造、电信和移动通信等不同领域开展业务。
It’s important to note that companies may change their business domains often. A canonical example of this is Nokia, which over the years has operated in fields as diverse as wood processing, rubber manufacturing, telecommunications, and mobile communications.
为实现其业务领域的目标和目标,一家公司必须在多个子域中运行. 子域是业务活动的细粒度区域。公司的所有子域构成其业务域:它向客户提供的服务。实施单个子域不足以使公司成功;它只是总体系统中的一个组成部分。子域必须相互交互以实现公司在其业务域中的目标。例如,星巴克可能因其咖啡而广为人知,但要打造成功的连锁咖啡店需要的不仅仅是知道如何制作优质咖啡。您还必须在有效地点购买或租赁房地产、雇用人员和管理财务等活动。这些子域本身都不会成为一家盈利的公司。所有这些都是公司能够在其业务领域竞争的必要条件。
To achieve its business domain’s goals and targets, a company has to operate in multiple subdomains. A subdomain is a fine-grained area of business activity. All of a company’s subdomains form its business domain: the service it provides to its customers. Implementing a single subdomain is not enough for a company to succeed; it’s just one building block in the overarching system. The subdomains have to interact with each other to achieve the company’s goals in its business domain. For example, Starbucks may be most recognized for its coffee, but building a successful coffeehouse chain requires more than just knowing how to make great coffee. You also have to buy or rent real estate at effective locations, hire personnel, and manage finances, among other activities. None of these subdomains on its own will make a profitable company. All of them together are necessary for a company to be able to compete in its business domain(s).
正如软件系统包含各种架构组件(数据库、前端应用程序、后端服务等)一样,子域具有不同的战略/业务价值。领域驱动设计区分三种类型的子域:核心、通用和支持。让我们从公司战略的角度看看它们有何不同。
Just as a software system comprises various architectural components—databases, frontend applications, backend services, and others—subdomains bear different strategic/business values. Domain-driven design distinguishes between three types of subdomains: core, generic, and supporting. Let’s see how they differ from a company strategy point of view.
核心子域是公司的工作与其竞争对手不同。这可能涉及发明新产品或服务或通过优化现有流程来降低成本。
A core subdomain is what a company does differently from its competitors. This may involve inventing new products or services or reducing costs by optimizing existing processes.
让我们以优步为例。最初,该公司提供了一种新颖的交通方式:拼车。随着竞争对手的追赶,优步找到了优化和发展其核心业务的方法:例如,通过匹配前往同一方向的乘客来降低成本。
Let’s take Uber as an example. Initially, the company provided a novel form of transportation: ridesharing. As its competitors caught up, Uber found ways to optimize and evolve its core business: for example, reducing costs by matching riders heading in the same direction.
Uber 的核心子域影响其利润。这就是该公司区别于竞争对手的方式。这是公司为客户提供更好服务和/或最大化盈利能力的战略。为了保持竞争优势,核心子领域涉及发明、智能优化、商业知识或其他知识产权。
Uber’s core subdomains affect its bottom line. This is how the company differentiates itself from its competitors. This is the company’s strategy for providing better service to its customers and/or maximizing its profitability. To maintain a competitive advantage, core subdomains involve inventions, smart optimizations, business know-how, or other intellectual property.
考虑另一个例子:谷歌搜索的排名算法。在撰写本文时,谷歌的广告平台占其利润的大部分。也就是说,Google Ads 不是一个子域,而是一个单独的业务域,子域包含它,包括其云计算服务(Google Cloud Platform)、生产力和协作工具(Google Workspaces)以及 Google 母公司 Alphabet 涉足的其他领域公司, 经营. 但是谷歌搜索及其排名算法呢?虽然搜索引擎不是付费服务,但它是 Google Ads 最大的展示平台。它提供出色搜索结果的能力是推动流量的动力,因此,它是广告平台的重要组成部分。由于算法中的错误或竞争对手提供更好的搜索服务而提供次优搜索结果将损害广告业务的收入。所以,对于谷歌来说,排名算法是一个核心子域。
Consider another example: Google Search’s ranking algorithm. At the time of this writing, Google’s advertising platform accounts for the majority of its profits. That said, Google Ads is not a subdomain, but rather a separate business domain with subdomains comprising it, among its cloud computing service (Google Cloud Platform), productivity and collaboration tools (Google Workspaces), and other fields in which Alphabet, Google’s parent company, operates. But what about Google Search and its ranking algorithm? Although the search engine is not a paid service, it serves as the largest display platform for Google Ads. Its ability to provide excellent search results is what drives traffic, and subsequently, it is an important component of the Ads platform. Serving suboptimal search results due to a bug in the algorithm or a competitor coming up with an even better search service will hurt the ad business’s revenue. So, for Google, the ranking algorithm is a core subdomain.
一个易于实现的核心子域只能提供短暂的竞争优势。因此,核心子域自然是复杂的。继续以优步为例,该公司不仅通过拼车创造了一个新的市场空间,还通过有针对性地使用技术打破了已有数十年历史的单体架构,即出租车行业。通过了解其业务领域,优步能够设计出一种更可靠、更透明的交通方式。公司的核心业务应该有很高的进入壁垒;竞争对手应该很难复制或模仿该公司的解决方案。
A core subdomain that is simple to implement can only provide a short-lived competitive advantage. Therefore, core subdomains are naturally complex. Continuing with the Uber example, the company not only created a new marketspace with ridesharing, it disrupted a decades-old monolithic architecture, the taxi industry, through targeted use of technology. By understanding its business domain, Uber was able to design a more reliable and transparent method of transportation. There should be high entry barriers for a company’s core business; it should be hard for competitors to copy or imitate the company’s solution.
重要的是要注意核心子域不一定是技术性的。并非所有业务问题都可以通过算法或其他技术解决方案来解决。一个公司的竞争优势可以有多种来源。
It’s important to note that core subdomains are not necessarily technical. Not all business problems are solved through algorithms or other technical solutions. A company’s competitive advantage can come from various sources.
例如,考虑一家在线销售其产品的珠宝制造商。在线商店很重要,但它不是核心子域。珠宝设计是。该公司可以使用现有的现成在线商店引擎,但不能将其珠宝设计外包。设计是顾客购买珠宝制造商的产品并记住品牌的原因。
Consider, for example, a jewelry maker selling its products online. The online shop is important, but it’s not a core subdomain. The jewelry design is. The company can use an existing off-the-shelf online shop engine, but it cannot outsource the design of its jewelry. The design is the reason customers buy the jewelry maker’s products and remember the brand.
作为一个更复杂的例子,想象一家专门从事人工欺诈检测的公司。该公司培训其分析师检查有问题的文件并标记潜在的欺诈案件。您正在构建分析师正在使用的软件系统。它是核心子域吗?不。核心子域是分析师正在做的工作。您正在构建的系统与欺诈分析无关,它只是显示文档并跟踪分析师的评论。
As a more intricate example, imagine a company that specializes in manual fraud detection. The company trains its analysts to go over questionable documents and flag potential fraud cases. You are building the software system the analysts are working with. Is it a core subdomain? No. The core subdomain is the work the analysts are doing. The system you are building has nothing to do with fraud analysis, it just displays the documents and tracks the analysts’ comments.
通用子域是业务活动所有公司都以同样的方式运作。与核心子域一样,通用子域通常很复杂且难以实现。但是,通用子域不会为公司提供任何竞争优势。这里不需要创新或优化:经过实战检验的实施广泛可用,所有公司都在使用它们。
Generic subdomains are business activities that all companies are performing in the same way. Like core subdomains, generic subdomains are generally complex and hard to implement. However, generic subdomains do not provide any competitive edge for the company. There is no need for innovation or optimization here: battle-tested implementations are widely available, and all companies use them.
例如,大多数系统需要对其用户进行身份验证和授权。与其发明专有的身份验证机制,不如使用现有的解决方案更有意义。这样的解决方案可能更可靠、更安全,因为它已经被许多其他有相同需求的公司测试过。
For example, most systems need to authenticate and authorize their users. Instead of inventing a proprietary authentication mechanism, it makes more sense to use an existing solution. Such a solution is likely to be more reliable and secure since it has already been tested by many other companies that have the same needs.
回到珠宝制造商在线销售产品的示例,珠宝设计是一个核心子域,而在线商店是一个通用子域。与其竞争对手使用相同的在线零售平台——相同的通用解决方案——不会影响珠宝制造商的竞争优势。
Going back to the example of a jewelry maker selling its products online, jewelry design is a core subdomain, but the online shop is a generic subdomain. Using the same online retail platform—the same generic solution—as its competitors would not impact the jewelry maker’s competitive advantage.
顾名思义,支持子域支持公司的业务。然而,与核心子域相反,支持子域不提供任何竞争优势。
As the name suggests, supporting subdomains support the company’s business. However, contrary to core subdomains, supporting subdomains do not provide any competitive advantage.
例如,考虑一家在线广告公司,其核心子领域包括将广告与访问者匹配、优化广告效果以及最小化广告空间成本。然而,要在这些领域取得成功,公司需要对其创意材料进行分类。公司存储和索引其物理创意材料(例如横幅和登录页面)的方式不会影响其利润。在那个领域没有什么可以发明或优化的。另一方面,创意目录对于实施公司的广告管理和服务系统至关重要。这使得内容编目解决方案成为公司的支持子域之一。
For example, consider an online advertising company whose core subdomains include matching ads to visitors, optimizing the ads’ effectiveness, and minimizing the cost of ad space. However, to achieve success in these areas, the company needs to catalog its creative materials. The way the company stores and indexes its physical creative materials, such as banners and landing pages, does not impact its profits. There is nothing to invent or optimize in that area. On the other hand, the creative catalog is essential for implementing the company’s advertising management and serving systems. That makes the content cataloging solution one of the company’s supporting subdomains.
支持子域的显着特征是解决方案业务逻辑的复杂性。支持子域很简单。他们的业务逻辑主要类似于数据输入屏幕和 ETL(提取、转换、加载)操作;即所谓的 CRUD(创建、读取、更新和删除)接口。这些活动领域不会为公司提供任何竞争优势,因此不需要很高的进入壁垒。
The distinctive characteristic of supporting subdomains is the complexity of the solution’s business logic. Supporting subdomains are simple. Their business logic resembles mostly data entry screens and ETL (extract, transform, load) operations; that is, the so-called CRUD (create, read, update, and delete) interfaces. These activity areas do not provide any competitive advantage for the company, and therefore do not require high entry barriers.
现在我们对三种类型的业务子域有了更深入的了解,让我们从其他角度探讨它们的差异,看看它们如何影响战略软件设计决策。
Now that we have a greater understanding of the three types of business subdomains, let’s explore their differences from additional angles and see how they affect strategic software design decisions.
只有核心子域才能为公司提供竞争优势。核心子域是公司将自己与竞争对手区分开来的战略。
Only core subdomains provide a competitive advantage to a company. Core subdomains are the company’s strategy for differentiating itself from its competitors.
根据定义,通用子域不能成为任何竞争优势。这些是通用解决方案——公司及其竞争对手使用的相同解决方案。
Generic subdomains, by definition, cannot be a source for any competitive advantage. These are generic solutions—the same solutions used by the company and its competitors.
支持子域进入门槛低,不能提供竞争优势。通常,一家公司不会介意其竞争对手复制其支持的子域——这不会影响其在行业中的竞争力。相反,从战略上讲,公司更希望其支持的子域是通用的、现成的解决方案,从而消除了设计和构建其实现的需要。您将在第 11 章中详细了解支持子域变成通用子域的情况,以及其他可能的排列。附录 A中将概述此类场景的真实案例研究。
Supporting subdomains have low entry barriers and cannot provide a competitive advantage either. Usually, a company wouldn’t mind its competitors copying its supporting subdomains—this won’t affect its competitiveness in the industry. On the contrary, strategically the company would prefer its supporting subdomains to be generic, ready-made solutions, thus eliminating the need to design and build their implementation. You will learn in detail about such cases of supporting subdomains turning into generic subdomains, as well as other possible permutations, in Chapter 11. A real-life case study of such a scenario will be outlined in Appendix A.
公司能够解决的问题越复杂,它能提供的商业价值就越大。复杂的问题不仅限于向消费者提供服务。例如,一个复杂的问题可以使业务更加优化和高效。例如,以较低的运营成本提供与竞争对手相同水平的服务也是一种竞争优势。
The more complex the problems a company is able to tackle, the more business value it can provide. The complex problems are not limited to delivering services to consumers. A complex problem can be, for example, making the business more optimized and efficient. For example, providing the same level of service as competitors do, but at lower operational costs, is a competitive advantage as well.
从更技术的角度来看,重要的是要识别组织的子域,因为不同类型的子域具有不同的复杂程度。在设计软件时,我们必须选择适应业务需求复杂性的工具和技术。因此,识别子域对于设计完善的软件解决方案至关重要。
From a more technical perspective, it’s important to identify the organization’s subdomains, because the different types of subdomains have different levels of complexity. When designing software, we have to choose tools and techniques that accommodate the complexity of the business requirements. Therefore, identifying subdomains is essential for designing a sound software solution.
支持子域的业务逻辑很简单。这些都是基本的ETL操作和CRUD接口,业务逻辑一目了然。通常,它不会超出验证输入或将数据从一种结构转换为另一种结构的范围。
Supporting subdomains’ business logic is simple. These are basic ETL operations and CRUD interfaces, and the business logic is obvious. Often, it doesn’t go beyond validating inputs or converting data from one structure to another.
通用子域要复杂得多。其他人已经投入时间和精力来解决这些问题应该有充分的理由。这些解决方案既不简单也不琐碎。例如,考虑加密算法或身份验证机制。
Generic subdomains are much more complicated. There should be a good reason why others have already invested time and effort in solving these problems. These solutions are neither simple nor trivial. Consider, for example, encryption algorithms or authentication mechanisms.
从知识可用性的角度来看,通用子域是“已知的未知数”。这些是你知道你不知道的事情。此外,这种知识很容易获得。您可以使用行业公认的最佳实践,也可以在需要时聘请专门从事该领域的顾问来帮助设计定制解决方案。
From a knowledge availability perspective, generic subdomains are “known unknowns.” These are the things that you know you don’t know. Furthermore, this knowledge is readily available. You can either use industry-accepted best practices or, if needed, hire a consultant specializing in the area to help design a custom solution.
核心子域很复杂。他们应该努力竞争对手尽可能复制——公司的盈利能力取决于此。这就是为什么在战略上,公司正在寻求解决复杂问题作为他们的核心子领域。
Core subdomains are complex. They should be as hard for competitors to copy as possible—the company’s profitability depends on it. That’s why strategically, companies are looking to solve complex problems as their core subdomains.
有时可能很难区分核心子域和支持子域。复杂性是一个有用的指导原则。询问有问题的子域是否可以变成副业。有人会自己付钱吗?如果是这样,这是一个核心子域。类似的推理适用于区分支持子域和通用子域:破解您自己的实现比集成外部实现更简单、成本更低吗?如果是这样,这是一个支持子域。
At times it may be challenging to differentiate between core and supporting subdomains. Complexity is a useful guiding principle. Ask whether the subdomain in question can be turned into a side business. Would someone pay for it on its own? If so, this is a core subdomain. Similar reasoning applies for differentiating supporting and generic subdomains: would it be simpler and cheaper to hack your own implementation, rather than integrating an external one? If so, this is a supporting subdomain.
从更技术的角度来看,确定核心子域很重要其复杂性将影响软件设计。正如我们之前讨论的,核心子域不一定与软件相关。识别与软件相关的核心子域的另一个有用的指导原则是评估您必须在代码中建模和实现的业务逻辑的复杂性。业务逻辑是否类似于用于数据输入的 CRUD 接口,或者您是否必须实施复杂的算法或由复杂的业务规则和不变量编排的业务流程?前者是辅助子域的标志,后者是典型的核心子域。
From a more technical perspective, it’s important to identify the core subdomains whose complexity will affect software design. As we discussed earlier, a core subdomain is not necessarily related to software. Another useful guiding principle for identifying software-related core subdomains is to evaluate the complexity of the business logic that you will have to model and implement in code. Does the business logic resemble CRUD interfaces for data entry, or do you have to implement complex algorithms or business processes orchestrated by complex business rules and invariants? In the former case, it’s a sign of a supporting subdomain, while the latter is a typical core subdomain.
图 1-1中的图表表示三种类型的子域在业务差异化和业务逻辑复杂性方面的相互作用。支持子域和通用子域之间的交集是一个灰色区域:它可以走任何一条路。如果存在支持子域功能的通用解决方案,则生成的子域类型取决于集成通用解决方案是否比从头实现功能更简单和/或更便宜。
The chart in Figure 1-1 represents the interplay between the three types of subdomains in terms of business differentiation and business logic complexity. The intersection between the supporting and generic subdomains is a gray area: it can go either way. If a generic solution exists for a supporting subdomain’s functionality, the resultant subdomain type depends on whether it’s simpler and/or cheaper to integrate the generic solution than it is to implement the functionality from scratch.
如前所述,核心子域可以经常更改。如果一个问题可以在第一次尝试中解决,这可能不是一个好的竞争优势——竞争对手会很快赶上来。因此,核心子域的解决方案应运而生。必须尝试、改进和优化不同的实现。此外,核心子域的工作从未完成。公司不断创新和发展核心子领域。这些更改以添加新功能或优化现有功能的形式出现。无论哪种方式,其核心子域的不断发展对于公司保持领先于竞争对手至关重要。
As mentioned previously, core subdomains can change often. If a problem can be solved on the first attempt, it’s probably not a good competitive advantage—competitors will catch up fast. Consequently, solutions for core subdomains are emergent. Different implementations have to be tried out, refined, and optimized. Moreover, the work on core subdomains is never done. Companies continuously innovate and evolve core subdomains. The changes come in the form of adding new features or optimizing existing functionality. Either way, the constant evolution of its core subdomains is essential for a company to stay ahead of its competitors.
与核心子域相反,支持子域不经常更改。它们不会为公司提供任何竞争优势,因此与在核心子域中投入的相同努力相比,支持子域的发展提供的商业价值微乎其微。
Contrary to the core subdomains, supporting subdomains do not change often. They do not provide any competitive advantage for the company, and therefore the evolution of a supporting subdomain provides a minuscule business value compared to the same effort invested in a core subdomain.
尽管有现有的解决方案,但通用子域会随着时间而改变。这些更改可以以安全补丁、错误修复或针对一般问题的全新解决方案的形式出现。
Despite having existing solutions, generic subdomains can change over time. The changes can come in the form of security patches, bug fixes, or entirely new solutions to the generic problems.
核心子域为公司提供了竞争能力与业内其他参与者。这是一项关键业务责任,但这是否意味着支持和通用子域不重要?当然不是。公司需要所有子域才能在其业务域中工作。子域就像基础构建块:拿走一个,整个结构可能会倒塌。也就是说,我们可以利用不同类型子域的固有属性来选择实现策略,以最有效的方式实现每种类型的子域。
Core subdomains provide the company its ability to compete with other players in the industry. That’s a business-critical responsibility, but does it mean that supporting and generic subdomains are not important? Of course not. All subdomains are required for the company to work in its business domain. The subdomains are like foundational building blocks: take one away and the whole structure may fall down. That said, we can leverage the inherent properties of the different types of subdomains to choose implementation strategies to implement each type of subdomain in the most efficient manner.
核心子域必须在内部实施。它们不能被购买或领养;这会破坏竞争优势的概念,因为公司的竞争对手也可以这样做。
Core subdomains have to be implemented in-house. They cannot be bought or adopted; that would undermine the notion of competitive advantage, as the company’s competitors would be able to do the same.
将核心子域的实施外包也是不明智的。这是一项战略投资。在核心子域上偷工减料不仅在短期内有风险,而且从长远来看可能会产生致命的后果:例如,无法支持公司目标的无法维护的代码库。组织最熟练的人才应该被分配到其核心子领域工作。此外,在内部实施核心子域使公司能够更快地进行更改和改进解决方案,从而在更短的时间内建立竞争优势。
It would also be unwise to outsource the implementation of a core subdomain. It is a strategic investment. Cutting corners on a core subdomain is not only risky in the short term but can have fatal consequences in the long run: for example, unmaintainable codebases that cannot support the company’s goals and objectives. The organization’s most skilled talent should be assigned to work on its core subdomains. Furthermore, implementing core subdomains in-house allows the company to make changes and evolve the solution more quickly, and therefore build the competitive advantage in less time.
由于核心子域的要求是预计会经常和持续地改变,解决方案必须是可维护和易于发展的。因此,核心子域需要实施最先进的工程技术。
Since core subdomains’ requirements are expected to change often and continuously, the solution must be maintainable and easy to evolve. Thus, core subdomains require implementation of the most advanced engineering techniques.
由于通用子域很难但已经解决了问题,购买现成的产品或采用开源解决方案比投入时间和精力在内部实施通用子域更具成本效益。
Since generic subdomains are hard but already solved problems, it’s more cost-effective to buy an off-the-shelf product or adopt an open source solution than invest time and effort into implementing a generic subdomain in-house.
缺乏竞争优势使得避免实施是合理的内部支持子域。但是,与通用子域不同,没有现成的解决方案可用。因此,公司别无选择,只能自己实施支持子域。也就是说,业务逻辑的简单性和更改的不频繁性使得偷工减料变得容易。
Lack of competitive advantage makes it reasonable to avoid implementing supporting subdomains in-house. However, unlike generic subdomains, no ready-made solutions are available. So, a company has no choice but to implement supporting subdomains itself. That said, the simplicity of the business logic and infrequency of changes make it easy to cut corners.
支持子域不需要复杂的设计模式或其他高级工程技术。一个快速的应用程序开发框架将足以实现业务逻辑,而不会引入意外的复杂性。
Supporting subdomains do not require elaborate design patterns or other advanced engineering techniques. A rapid application development framework will suffice to implement the business logic without introducing accidental complexities.
从人员配置的角度来看,支持子域不需要高技能的技术能力,并且提供了培养有前途的人才的绝佳机会。保留团队中在应对核心子域的复杂挑战方面经验丰富的工程师。最后,业务逻辑的简单性使得支持子域成为外包的良好候选者。
From a staffing perspective, supporting subdomains do not require highly skilled technical aptitude and provide a great opportunity to train up-and-coming talent. Save the engineers on your team who are experienced in tackling complex challenges for the core subdomains. Finally, the simplicity of the business logic makes supporting subdomains a good candidate for outsourcing.
表 1-1总结了三种子域的不同之处。
Table 1-1 summarizes the aspects in which the three types of subdomains differ.
| 子域类型 | 竞争优势 | 复杂 | 挥发性 | 执行 | 问题 |
| 核 | 是的 | 高的 | 高的 | 内部 | 有趣的 |
| 通用的 | 不 | 高的 | 低的 | 购买/领养 | 解决了 |
| 配套 | 不 | 低的 | 低的 | 内部/外包 | 明显的 |
正如您已经看到的,识别子域及其在构建软件解决方案时,类型可以极大地帮助做出不同的设计决策。在后面的章节中,您将了解更多利用子域来简化软件设计过程的方法。但是我们如何真正识别子域及其边界呢?
As you can already see, identifying subdomains and their types can help considerably in making different design decisions when building software solutions. In later chapters, you will learn even more ways to leverage subdomains to streamline the software design process. But how do we actually identify the subdomains and their boundaries?
子域及其类型由公司定义业务战略:它的业务领域以及它如何使自己与众不同以与同一领域的其他公司竞争。在绝大多数软件项目中,子域以某种方式“已经存在”。然而,这并不意味着确定它们的界限总是容易和直接的。如果你向 CEO 询问他们公司的子域列表,你可能会得到一个茫然的眼神。他们不知道这个概念。因此,您必须自己进行域分析,以识别和分类起作用的子域。
The subdomains and their types are defined by the company’s business strategy: its business domains and how it differentiates itself to compete with other companies in the same field. In the vast majority of software projects, in one way or another the subdomains are “already there.” That doesn’t mean, however, that it is always easy and straightforward to identify their boundaries. If you ask a CEO for a list of their company’s subdomains, you will probably receive a blank stare. They are not aware of this concept. Therefore, you’ll have to do the domain analysis yourself to identify and categorize the subdomains at play.
一个好的起点是公司的部门和其他组织单位。例如,在线零售店可能包括仓库、客户服务、拣货、运输、质量控制和渠道管理等部门。然而,这些是相对粗粒度的活动领域。以客户服务部门为例。可以合理地假设它是一个支持子域,甚至是一个通用子域,因为此功能通常外包给第三方供应商。但是这些信息足以让我们做出合理的软件设计决策吗?
A good starting point is the company’s departments and other organizational units. For example, an online retail shop might include warehouse, customer service, picking, shipping, quality control, and channel management departments, among others. These, however, are relatively coarse-grained areas of activity. Take, for example, the customer service department. It’s reasonable to assume that it would be a supporting, or even a generic subdomain, as this function is often outsourced to third-party vendors. But is this information enough for us to make sound software design decisions?
粗粒度的子域是一个很好的起点,但细节决定成败。我们必须确保我们不会遗漏隐藏在错综复杂的业务功能中的重要信息。
Coarse-grained subdomains are a good starting point, but the devil is in the details. We have to make sure we are not missing important information hidden in the intricacies of the business function.
让我们回到客户服务部门的例子。如果我们调查它的内部运作,我们会发现一个典型的客户服务部门是由更细粒度的组件组成的,例如服务台系统、轮班管理和调度、电话系统等。当被视为单独的子域时,这些活动可以有不同的类型:虽然帮助台和电话系统是通用子域,但轮班管理是辅助子域,而公司可能会开发其巧妙的算法来将事件路由给在类似案例中取得成功的代理过去。路由算法需要分析传入的案例并识别过去经验中的相似之处——这两者都是重要的任务。由于路由算法允许公司提供比竞争对手更好的客户体验,路由算法是一个核心子域。这个例子在图 1-2。
Let’s go back to the example of the customer service department. If we investigate its inner workings, we will see that a typical customer service department is composed of finer-grained components, such as a help desk system, shift management and scheduling, telephone system, and so on. When viewed as individual subdomains, these activities can be of different types: while help desk and telephone systems are generic subdomains, shift management is a supporting one, while a company may develop its ingenious algorithm for routing incidents to agents having success with similar cases in the past. The routing algorithm requires analyzing incoming cases and identifying similarities in past experience—both of which are nontrivial tasks. Since the routing algorithm allows the company to provide a better customer experience than its competitors, the routing algorithm is a core subdomain. This example is demonstrated in Figure 1-2.
另一方面,我们不能无限地向下钻取,在越来越低的粒度级别寻找洞察力。你应该什么时候停止?
On the other hand, we cannot drill down indefinitely, looking for insights at lower and lower levels of granularity. When should you stop?
从技术角度来看,子域类似于一组相互关联、连贯的用例。这样的用例集通常涉及相同的参与者、业务实体,并且它们都操作一组密切相关的数据。
From a technical perspective, subdomains resemble sets of interrelated, coherent use cases. Such sets of use cases usually involve the same actor, the business entities, and they all manipulate a closely related set of data.
考虑图 1-3中所示的信用卡支付网关的用例图。用例与他们正在使用的数据和涉及的参与者紧密相关。因此,所有用例都构成了信用卡支付子域。
Consider the use case diagram for a credit card payment gateway shown in Figure 1-3. The use cases are tightly bound by the data they are working with and the involved actors. Hence, all of the use cases form the credit card payment subdomain.
我们可以使用“子域作为一组连贯的用例”的定义作为何时停止寻找更细粒度子域的指导原则。这些是子域最精确的边界。
We can use the definition of “subdomains as a set of coherent use cases” as a guiding principle for when to stop looking for finer-grained subdomains. These are the most precise boundaries of the subdomains.
您是否应该始终努力识别这种激光聚焦的子域边界?这对于核心子域来说绝对是必要的。核心子域是最重要的、易变的和复杂的。我们必须尽可能多地提炼它们,因为这将使我们能够提取所有通用和支持功能,并将精力投入到更有针对性的功能上。
Should you always strive to identify such laser-focused subdomain boundaries? It is definitely necessary for core subdomains. Core subdomains are the most important, volatile, and complex. It’s essential that we distill them as much as possible since that will allow us to extract all generic and supporting functionalities and invest the effort on a much more focused functionality.
对于支持和通用子域,蒸馏可以稍微放松。如果进一步深入研究没有揭示任何可以帮助您做出软件设计决策的新见解,那么它可能是一个停下来的好地方。例如,当所有更细粒度的子域都与原始子域属于同一类型时,就会发生这种情况。
The distillation can be somewhat relaxed for supporting and generic subdomains. If drilling down further doesn’t unveil any new insights that can help you make software design decisions, it can be a good place to stop. This can happen, for example, when all of the finer-grained subdomains are of the same type as the original subdomain.
考虑图 1-4中的示例。帮助台系统子域的进一步提炼用处不大,因为它不会透露任何战略信息,并且将使用粗粒度的现成工具作为解决方案。
Consider the example in Figure 1-4. Further distillation of the help desk system subdomain is less useful, as it doesn’t reveal any strategic information, and a coarse-grained, off-the-shelf tool will be used as the solution.
识别子域时要考虑的另一个重要问题是我们是否需要所有子域。
Another important question to consider when identifying the subdomains is whether we need all of them.
子域是一种减轻制作过程的工具软件设计决策。所有组织都可能有相当多的业务功能来推动他们的竞争优势,但与软件无关。我们在本章前面讨论的珠宝制造商只是一个例子。
Subdomains are a tool that alleviates the process of making software design decisions. All organizations likely have quite a few business functionalities that drive their competitive advantage but have nothing to do with software. The jewelry maker we discussed earlier in this chapter is but one example.
在寻找子域时,重要的是识别与软件无关的业务功能,承认它们,并关注与您正在处理的软件系统相关的业务方面。
When looking for subdomains, it’s important to identify business functions that are not related to software, acknowledge them as such, and focus on aspects of the business that are relevant to the software system you are working on.
让我们看看如何在实践中应用子域的概念并用它来做出一些战略设计决策。我将描述两家虚构的公司:Gigmaster 和 BusVNext。作为练习,当你阅读时,分析公司的业务领域。尝试为每家公司确定三种类型的子域。请记住,在现实生活中,一些业务需求是隐含的。
Let’s see how we can apply the notion of subdomains in practice and use it for making a number of strategic design decisions. I’m going to describe two fictitious companies: Gigmaster and BusVNext. As an exercise, while you are reading, analyze the companies’ business domains. Try to identify the three types of subdomains for each company. Remember that, as in real life, some of the business requirements are implicit.
免责声明:当然,我们无法通过阅读如此简短的描述来识别每个业务领域涉及的所有子域。也就是说,训练您识别和分类可用的子域就足够了。
Disclaimer: of course, we cannot identify all the subdomains involved in each business domain by reading such a short description. That said, it is enough to train you to identify and categorize the available subdomains.
Gigmaster 是一家门票销售和分销公司。它是移动应用程序分析用户的音乐库、流媒体服务帐户和社交媒体资料,以识别其用户可能有兴趣参加的附近节目。
Gigmaster is a ticket sales and distribution company. Its mobile app analyzes users’ music libraries, streaming service accounts, and social media profiles to identify nearby shows that its users would be interested in attending.
Gigmaster 的用户意识到他们的隐私。因此,所有用户的个人信息都是加密的。此外,为确保用户的罪恶感在任何情况下都不会泄露,该公司的推荐算法专门针对匿名数据。
Gigmaster’s users are conscious of their privacy. Hence, all users’ personal information is encrypted. Moreover, to ensure that users’ guilty pleasures won’t leak out under any circumstances, the company’s recommendation algorithm works exclusively on anonymized data.
为了改进应用程序的推荐,实施了一个新模块。它允许用户记录他们过去参加过的演出,即使门票不是通过 Gigmaster 购买的。
To improve the app’s recommendations, a new module was implemented. It allows users to log gigs they attended in the past, even if the tickets weren’t purchased through Gigmaster.
Gigmaster的业务领域是门票销售。这就是它为客户提供的服务。
Gigmaster’s business domain is ticket sales. That’s the service it provides to its customers.
Gigmaster 的主要竞争优势是其推荐引擎。该公司还非常重视用户的隐私,只处理匿名数据。最后,虽然没有明确提及,但我们可以推断出移动应用程序的用户体验也很重要。因此,Gigmaster 的核心子域是:
Gigmaster’s main competitive advantage is its recommendation engine. The company also takes its users’ privacy seriously and works only on anonymized data. Finally, although not mentioned explicitly, we can infer that the mobile app’s user experience is crucial as well. As such, Gigmaster’s core subdomains are:
推荐引擎
Recommendation engine
数据匿名化
Data anonymization
移动应用
Mobile app
我们可以识别和推断以下通用子域:
We can identify and infer the following generic subdomains:
加密,用于加密所有数据
Encryption, for encrypting all data
会计,因为公司是做销售业务的
Accounting, since the company is in the sales business
清算,向客户收费
Clearing, for charging its customers
身份验证和授权,用于识别其用户
Authentication and authorization, for identifying its users
最后,以下是支持的子域。这里的业务逻辑很简单,类似于 ETL 流程或 CRUD 接口:
Finally, the following are the supporting subdomains. Here the business logic is simple and resembles ETL processes or CRUD interfaces:
与音乐流媒体服务集成
Integration with music streaming services
与社交网络整合
Integration with social networks
参加演出模块
Attended-gigs module
了解起作用的子域及其类型之间的差异,我们已经可以做出几个战略设计决策:
Knowing the subdomains at play and the differences between their types, we can already make several strategic design decisions:
推荐引擎、数据匿名化和移动应用程序必须使用最先进的工程工具和技术在内部实施。这些模块将最常更改。
The recommendation engine, data anonymization, and mobile app have to be implemented in-house using the most advanced engineering tools and techniques. These modules are going to change the most often.
应使用现成或开源解决方案进行数据加密、计费、清算和身份验证。
Off-the-shelf or open source solutions should be used for data encryption, accounting, clearing, and authentication.
Integration with streaming services and social networks, as well as the module for attended gigs, can be outsourced.
BusVNext 是一家公共交通公司。它旨在为客户提供舒适的公共汽车乘坐体验,就像乘坐出租车一样。该公司在主要城市管理公交车队。
BusVNext is a public transportation company. It aims to provide its customers with bus rides that are comfortable, like catching a cab. The company manages fleets of buses in major cities.
BusVNext 客户可以通过移动应用程序订购乘车服务。在预定的发车时间,附近的公交车会即时调整路线,在指定的发车时间接客。
A BusVNext customer can order a ride through the mobile app. At the scheduled departure time, a nearby bus’s route will be adjusted on the fly to pick up the customer at the specified departure time.
公司面临的主要挑战是实施路由算法。它的要求是一个变体的“旅行商问题”。路由逻辑不断调整和优化。例如,统计数据显示取消乘车的主要原因是公交车到达的等待时间过长。因此,该公司调整了路线算法以优先考虑快速取件,即使这意味着延迟送件。为了进一步优化路线,BusVNext 与第三方提供商集成以提供交通状况和实时警报。
The company’s major challenge was implementing the routing algorithm. Its requirements are a variant of the “travelling salesman problem”. The routing logic is continuously adjusted and optimized. For example, statistics show the primary reason for canceled rides is the long wait time for a bus to arrive. So, the company adjusted the routing algorithm to prioritize fast pickups, even if that means delayed drop-offs. To optimize the routing even more, BusVNext integrates with third-party providers for traffic conditions and real-time alerts.
BusVNext 会不时推出特别折扣,以吸引新客户并平衡高峰时段和非高峰时段的乘车需求。
From time to time, BusVNext issues special discounts, both to attract new customers and to level the demand for rides over peak and off-peak hours.
BusVNext 为其客户提供优化的巴士乘坐服务。业务领域为公共交通。
BusVNext provides optimized bus rides to its customers. The business domain is public transportation.
BusVNext 的主要竞争优势在于其路由算法,该算法着眼于解决一个复杂的问题(“旅行推销员”),同时优先考虑不同的业务目标:例如,减少接送时间,即使这会增加整体行程长度。
BusVNext’s primary competitive advantage is its routing algorithm that takes a stab at solving a complex problem (“travelling salesman”) while prioritizing different business goals: for example, decreasing pickup times, even if it will increase overall ride lengths.
我们还看到,乘车数据不断被分析,以获取对客户行为的新见解。这些见解使公司能够通过优化路由算法来增加利润。最后,BusVNext 为其客户及其驱动程序提供的应用程序必须易于使用并提供方便的用户界面。
We also saw that the rides data is continuously analyzed for new insights into customers’ behaviors. These insights allow the company to increase its profits by optimizing the routing algorithm. Finally, BusVNext’s applications for its customers and its drivers have to be easy to use and provide a convenient user interface.
管理车队并非易事。巴士可能会遇到技术问题或需要维护。忽视这些可能会导致经济损失和服务水平下降。
Managing a fleet is not trivial. Buses may experience technical issues or require maintenance. Ignoring these may result in financial losses and a reduced level of service.
因此,BusVNext 的核心子域是:
Hence, BusVNext’s core subdomains are:
路由
Routing
分析
Analysis
移动应用用户体验
Mobile app user experience
车队的管理
Fleet management
路由算法还使用第三方公司(通用子域)提供的流量数据和警报。此外,BusVNext 接受客户付款,因此它必须实施会计和清算功能。BusVNext 的通用子域是:
The routing algorithm also uses traffic data and alerts provided by third-party companies—a generic subdomain. Moreover, BusVNext accepts payments from its customers, so it has to implement accounting and clearing functionalities. BusVNext’s generic subdomains are:
交通状况
Traffic conditions
会计
Accounting
计费
Billing
授权
Authorization
管理促销和折扣的模块支持公司的核心业务。也就是说,它本身并不是一个核心子域。它的管理界面类似于用于管理活动优惠券代码的简单 CRUD 界面。因此,这是一个典型的支持子域。
The module for managing promos and discounts supports the company’s core business. That said, it’s not a core subdomain by itself. Its management interface resembles a simple CRUD interface for managing active coupon codes. Therefore, this is a typical supporting subdomain.
了解起作用的子域及其类型之间的差异,我们已经可以做出一些战略设计决策:
Knowing the subdomains at play and the differences between their types, we can already make a number of strategic design decisions:
路由算法、数据分析、车队管理和应用程序可用性必须使用最精细的技术工具和模式在内部实施。
The routing algorithm, data analysis, fleet management, and app usability have to be implemented in-house using the most elaborate technical tools and patterns.
促销管理模块的实施可以外包。
Implementation of the promotions management module can be outsourced.
识别交通状况、授权用户以及管理财务记录和交易可以卸载到外部服务提供商。
Identifying traffic conditions, authorizing users, and managing financial records and transactions can be offloaded to external service providers.
现在我们已经清楚地了解了业务领域和子领域,让我们看看在后面的章节中我们会经常使用的另一个 DDD 术语:领域专家。领域专家是主题专家,他们了解我们将在代码中建模和实施的所有复杂业务。换句话说,领域专家是软件业务领域的知识权威。
Now that we have a clear understanding of business domains and subdomains, let’s take a look at another DDD term that we will use often in the following chapters: domain experts. Domain experts are subject matter experts who know all the intricacies of the business that we are going to model and implement in code. In other words, domain experts are knowledge authorities in the software’s business domain.
领域专家既不是收集需求的分析师,也不是设计系统的工程师。领域专家代表业务。他们是首先发现业务问题的人,也是所有业务知识的来源。系统分析师和工程师正在将他们的业务领域心智模型转化为软件需求和源代码。
The domain experts are neither the analysts gathering the requirements nor the engineers designing the system. Domain experts represent the business. They are the people who identified the business problem in the first place and from whom all business knowledge originates. Systems analysts and engineers are transforming their mental models of the business domain into software requirements and source code.
根据经验,领域专家要么是提出需求的人,要么是软件的最终用户。该软件应该解决他们的问题。
As a rule of thumb, domain experts are either the people coming up with requirements or the software’s end users. The software is supposed to solve their problems.
领域专家的专业知识可以有不同的范围。一些主题专家将详细了解整个业务领域的运作方式,而其他人则专注于特定的子领域。例如,在在线广告代理机构中,领域专家可能是活动经理、媒体采购员、分析师和其他业务利益相关者。
The domain experts’ expertise can have different scopes. Some subject matter experts will have a detailed understanding of how the entire business domain operates, while others will specialize in particular subdomains. For example, in an online advertising agency, the domain experts would be campaign managers, media buyers, analysts, and other business stakeholders.
在本章中,我们介绍了用于理解公司业务活动的领域驱动设计工具。如您所见,这一切都始于业务领域:业务运营所在的区域以及它向客户提供的服务。
In this chapter, we covered domain-driven design tools for making sense of a company’s business activity. As you’ve seen, it all starts with the business domain: the area the business operates in and the service it provides to its clients.
您还了解了在业务领域取得成功并将公司与竞争对手区分开来所需的不同构建模块:
You also learned about the different building blocks required to achieve success in a business domain and differentiate the company from its competitors:
最后,您了解到领域专家是业务的主题专家。他们对公司的业务领域或其一个或多个子领域有深入的了解,对项目的成功至关重要。
Finally, you learned that domain experts are the business’s subject matter experts. They have in-depth knowledge of the company’s business domain or one or more of its subdomains and are critical to a project’s success.
哪个子域没有竞争优势?
核
通用的
配套
乙丙
Which of the subdomains provide(s) no competitive advantage?
Core
Generic
Supporting
B and C
所有竞争对手可能针对哪个子域使用相同的解决方案?
核。
通用的。
支持。
以上都不是。公司应始终将自己与竞争对手区分开来。
For which subdomain might all competitors use the same solutions?
Core.
Generic.
Supporting.
None of the above. The company should always differentiate itself from its competitors.
哪个子域预计最常更改?
核。
通用的。
支持。
不同子域类型的波动性没有差异。
Which subdomain is expected to change the most often?
Core.
Generic.
Supporting.
There is no difference in volatility of the different subdomain types.
考虑一下 WolfDesk 的描述(参见前言),这是一家提供服务台工单管理系统的公司:
Consider the description of WolfDesk (see the Preface), a company that provides a help desk ticket management system:
WolfDesk 的业务领域是什么?
What is WolfDesk’s business domain?
WolfDesk 的核心子域是什么?
What is/are WolfDesk’s core subdomain(s)?
WolfDesk 的支持子域是什么?
What is/are WolfDesk’s supporting subdomain(s)?
WolfDesk 的通用子域是什么?
What is/are WolfDesk’s generic subdomain(s)?
在生产中发布的是开发人员的(错误)理解,而不是领域专家的知识。
阿尔贝托布兰多里尼
It’s developers’ (mis)understanding, not domain experts’ knowledge, that gets released in production.
Alberto Brandolini
在上一章中,我们开始探索业务领域。您学习了如何识别公司的业务领域或活动领域,并分析其在其中竞争的战略;即其业务子域的边界和类型。
In the previous chapter, we started exploring business domains. You learned how to identify a company’s business domains, or areas of activity, and analyze its strategy to compete in them; that is, its business subdomains’ boundaries and types.
本章继续业务领域分析的主题,但在不同的维度:深度。它着重于子域内部发生的事情:它的业务功能和逻辑。您将学习用于有效沟通和知识共享的领域驱动设计工具:无处不在的语言。在这里,我们将使用它来了解业务领域的复杂性。在本书的后面,我们将使用它在软件中建模和实现他们的业务逻辑。
This chapter continues the topic of business domain analysis but in a different dimension: depth. It focuses on what happens inside a subdomain: its business function and logic. You will learn the domain-driven design tool for effective communication and knowledge sharing: the ubiquitous language. Here we will use it to learn the intricacies of business domains. Later in the book we will use it to model and implement their business logic in software.
我们正在构建的软件系统是解决方案业务问题。在这种情况下,问题这个词不像数学问题或你可以解决并完成的谜语。在业务领域的上下文中,“问题”具有更广泛的含义。业务问题可能是与优化工作流和过程、最小化体力劳动、管理资源、支持决策、管理数据等相关的挑战。
The software systems we are building are solutions to business problems. In this context, the word problem doesn’t resemble a mathematical problem or a riddle that you can solve and be done with. In the context of business domains, “problem” has a broader meaning. A business problem can be challenges associated with optimizing workflows and processes, minimizing manual labor, managing resources, supporting decisions, managing data, and so on.
业务问题同时出现在业务域和子域级别。公司的目标是为其客户的问题提供解决方案。回到第 1 章中的 FedEx 示例,该公司的客户需要在有限的时间内运送包裹,因此它优化了运送流程。
Business problems appear both at the business domain and subdomain levels. A company’s goal is to provide a solution for its customers’ problems. Going back to the FedEx example in Chapter 1, that company’s customers need to ship packages in limited time frames, so it optimizes the shipping process.
子域是更细粒度的问题域,其目标是针对特定的业务能力提供解决方案。知识管理子域优化了存储和检索信息的过程。清算子域优化执行金融交易的过程。会计子域跟踪公司的资金。
Subdomains are finer-grained problem domains whose goal is to provide solutions for specific business capabilities. A knowledge management subdomain optimizes the process of storing and retrieving information. A clearing subdomain optimizes the process of executing financial transactions. An accounting subdomain keeps track of the company’s funds.
要设计一个有效的软件解决方案,我们必须掌握至少业务领域的基本知识。正如我们在第 1 章中讨论的那样,这些知识属于领域专家:专门研究和理解业务领域的所有复杂性是他们的工作。我们绝不应该,也不能成为领域专家。也就是说,了解领域专家并使用他们使用的相同业务术语对我们来说至关重要。
To design an effective software solution, we have to grasp at least the basic knowledge of the business domain. As we discussed in Chapter 1, this knowledge belongs to domain experts: it’s their job to specialize in and comprehend all the intricacies of the business domain. By no means should we, nor can we, become domain experts. That said, it’s crucial for us to understand domain experts and to use the same business terminology they use.
为了有效,该软件必须模仿领域专家思考问题的方式——他们的心智模型。如果不了解业务问题和需求背后的原因,我们的解决方案将仅限于将业务需求“翻译”成源代码。如果需求错过了关键的边缘情况怎么办?或者未能描述业务概念,从而限制我们实施支持未来需求的模型的能力?
To be effective, the software has to mimic the domain experts’ way of thinking about the problem—their mental models. Without an understanding of the business problem and the reasoning behind the requirements, our solutions will be limited to “translating” business requirements into source code. What if the requirements miss a crucial edge case? Or fail to describe a business concept, limiting our ability to implement a model that will support future requirements?
正如 Alberto Brandolini 1所说,软件开发是一个学习过程;工作代码是一个副作用。软件项目的成功取决于领域专家和软件工程师之间知识共享的有效性。我们必须了解问题才能解决它。
As Alberto Brandolini1 says, software development is a learning process; working code is a side effect. A software project’s success depends on the effectiveness of knowledge sharing between domain experts and software engineers. We have to understand the problem in order to solve it.
领域专家和软件工程师之间有效的知识共享需要有效的沟通。让我们来看看软件项目中有效沟通的常见障碍。
Effective knowledge sharing between domain experts and software engineers requires effective communication. Let’s take a look at the common impediments to effective communication in software projects.
可以肯定地说,几乎所有软件项目都需要不同角色的利益相关者的协作:领域专家、产品所有者、工程师、UI 和 UX 设计师、项目经理、测试人员、分析师等。与任何协作努力一样,结果取决于所有各方的合作程度。例如,所有利益相关者是否就正在解决的问题达成一致?他们正在构建的解决方案如何——他们是否对其功能和非功能需求持有任何相互冲突的假设?就所有与项目相关的事项达成一致和一致对于项目的成功至关重要。
It’s safe to say that almost all software projects require the collaboration of stakeholders in different roles: domain experts, product owners, engineers, UI and UX designers, project managers, testers, analysts, and others. As in any collaborative effort, the outcome depends on how well all those parties can work together. For example, do all stakeholders agree on what problem is being solved? What about the solution they are building—do they hold any conflicting assumptions about its functional and nonfunctional requirements? Agreement and alignment on all project-related matters are essential to a project’s success.
对软件项目失败原因的研究表明,有效的沟通对于知识共享和项目成功至关重要。2然而,尽管它很重要,但在软件项目中很少观察到有效的沟通。通常,商人和工程师之间没有直接的互动。相反,领域知识从领域专家向下推给工程师。它是通过扮演调解人或“翻译”、系统/业务分析师、产品所有者和项目经理角色的人员来交付的。这种共同知识共享流程如图 2-1所示。
Research into why software projects fail has shown that effective communication is essential for knowledge sharing and project success.2 Yet, despite its importance, effective communication is rarely observed in software projects. Often, businesspeople and engineers have no direct interaction with one another. Instead, domain knowledge is pushed down from domain experts to engineers. It is delivered through people playing the role of mediators, or “translators,” systems/business analysts, product owners, and project managers. Such common knowledge sharing flow is illustrated in Figure 2-1.
在传统的软件开发生命周期中,领域知识被“翻译”成工程师友好的形式称为分析模型,这是对系统需求的描述,而不是对其背后业务领域的理解。虽然意图可能是好的,但这种调解对知识共享是有害的。在任何翻译中,信息都会丢失;在这种情况下,对于解决业务问题至关重要的领域知识在传递给软件工程师的过程中丢失了。这不是典型软件项目中唯一的此类翻译。分析模型被翻译成软件设计模型(软件设计文档,被翻译成实现模型或源代码本身)。正如经常发生的那样,文档很快就会过时。源代码用于将业务领域知识传达给稍后将维护该项目的软件工程师。图 2-2说明了在代码中实现领域知识所需的不同翻译。
During the traditional software development lifecycle, the domain knowledge is “translated” into an engineer-friendly form known as an analysis model, which is a description of the system’s requirements rather than an understanding of the business domain behind it. While the intentions may be good, such mediation is hazardous to knowledge sharing. In any translation, information is lost; in this case, domain knowledge that is essential for solving business problems gets lost on its way to the software engineers. This is not the only such translation on a typical software project. The analysis model is translated into the software design model (a software design document, which is translated into an implementation model or the source code itself). As often happens, documents go out of date quickly. The source code is used to communicate business domain knowledge to software engineers who will maintain the project later. Figure 2-2 illustrates the different translations needed for domain knowledge to be implemented in code.
这样的软件开发过程类似于儿童游戏 Telephone: 3消息或领域知识经常会被扭曲。这些信息导致软件工程师实施了错误的解决方案,或者实施了正确的解决方案但解决了错误的问题。无论哪种情况,结果都是一样的:一个失败的软件项目。
Such a software development process resembles the children’s game Telephone:3 the message, or domain knowledge, often becomes distorted. The information leads to software engineers implementing the wrong solution, or the right solution but to the wrong problems. In either case, the outcome is the same: a failed software project.
领域驱动设计提出了一种从领域专家到软件工程师获取知识的更好方法:使用通用语言。
Domain-driven design proposes a better way to get the knowledge from domain experts to software engineers: by using a ubiquitous language.
使用通用语言是领域驱动设计的基石实践。这个想法简单明了:如果各方需要有效沟通,而不是依赖翻译,他们必须说同一种语言。
Using a ubiquitous language is the cornerstone practice of domain-driven design. The idea is simple and straightforward: if parties need to communicate efficiently, instead of relying on translations, they have to speak the same language.
虽然这个概念是边缘常识,正如伏尔泰所说,“常识并不那么普遍。” 传统的软件开发生命周期意味着以下翻译:
Although this notion is borderline common sense, as Voltaire said, “common sense is not so common.” The traditional software development lifecycle implies the following translations:
将领域知识转化为分析模型
Domain knowledge into an analysis model
需求分析模型
Analysis model into requirements
需求融入系统设计
Requirements into system design
系统设计成源代码
System design into source code
领域驱动设计不是不断翻译领域知识,而是培养一种描述业务领域的单一语言:无处不在的语言。
Instead of continuously translating domain knowledge, domain-driven design calls for cultivating a single language for describing the business domain: the ubiquitous language.
所有与项目相关的利益相关者——软件工程师、产品所有者、领域专家、UI/UX 设计师——在描述业务领域时都应该使用通用语言。最重要的是,领域专家在推理业务领域时必须能够自如地使用通用语言;这种语言将代表业务领域和领域专家的心智模型。
All project-related stakeholders—software engineers, product owners, domain experts, UI/UX designers—should use the ubiquitous language when describing the business domain. Most importantly, domain experts must be comfortable using the ubiquitous language when reasoning about the business domain; this language will represent both the business domain and the domain experts’ mental models.
只有通过持续使用无处不在的语言及其术语,才能培养项目所有利益相关者之间的共同理解。
Only through the continuous use of the ubiquitous language and its terms can a shared understanding among all of the project’s stakeholders be cultivated.
强调无处不在的语言是企业的语言是至关重要的。因此,它应该只包含与业务领域相关的术语。没有技术术语!向业务领域专家教授单例和抽象工厂不是您的目标。无处不在的语言旨在用易于理解的术语来构建领域专家对业务领域的理解和心智模型。
It’s crucial to emphasize that the ubiquitous language is the language of the business. As such, it should consist of business domain–related terms only. No technical jargon! Teaching business domain experts about singletons and abstract factories is not your goal. The ubiquitous language aims to frame the domain experts’ understanding and mental models of the business domain in terms that are easy to understand.
假设我们正在开发一个广告活动管理系统。考虑以下语句:
Let’s say we are working on an advertising campaign management system. Consider the following statements:
广告活动可以展示不同的创意材料。
An advertising campaign can display different creative materials.
仅当至少有一个展示位置处于活动状态时,才能发布活动。
A campaign can be published only if at least one of its placements is active.
销售佣金在交易获得批准后入账。
Sales commissions are accounted for after transactions are approved.
所有这些陈述都是用业务语言制定的。也就是说,它们反映了领域专家对业务领域的看法。
All of these statements are formulated in the language of the business. That is, they reflect the domain experts’ view of the business domain.
另一方面,以下陈述是严格的技术性陈述,因此不符合无处不在的语言的概念:
On the other hand, the following statements are strictly technical and thus do not fit the notion of the ubiquitous language:
广告 iframe 显示一个 HTML 文件。
The advertisement iframe displays an HTML file.
仅当活动在活动展示位置表中至少有一条关联记录时,才能发布该活动。
A campaign can be published only if it has at least one associated record in the active-placements table.
销售佣金基于交易和批准销售表中的相关记录。
Sales commissions are based on correlated records from the transactions and approved-sales tables.
后面的这些陈述纯粹是技术性的,领域专家并不清楚。假设工程师只熟悉业务领域的这种技术的、面向解决方案的视图。在这种情况下,他们将无法完全理解业务逻辑或它为何以这种方式运作,这将限制他们建模和实施有效解决方案的能力。
These latter statements are purely technical and will be unclear to domain experts. Suppose engineers are only familiar with this technical, solution-oriented view of the business domain. In that case, they won’t be able to completely understand the business logic or why it operates the way it does, which will limit their ability to model and implement an effective solution.
无处不在的语言必须准确一致。它应该消除对假设的需要,并且应该使业务领域的逻辑明确。
The ubiquitous language must be precise and consistent. It should eliminate the need for assumptions and should make the business domain’s logic explicit.
由于歧义阻碍了交流,因此无处不在的语言的每个术语都应该有一个且只有一个含义。让我们看几个术语不明确的例子,以及如何改进它。
Since ambiguity hinders communication, each term of the ubiquitous language should have one and only one meaning. Let’s look at a few examples of unclear terminology and how it can be improved.
假设在某些业务领域中,保单一词具有多种含义:它可以表示监管规则或保险合同。确切的含义可以在人与人的互动中得出,具体取决于上下文。然而,软件并不能很好地处理歧义,而且在代码中对“策略”实体进行建模可能会很麻烦且具有挑战性。
Let’s say that in some business domain, the term policy has multiple meanings: it can mean a regulatory rule or an insurance contract. The exact meaning can be worked out in human-to-human interaction, depending on the context. Software, however, doesn’t cope well with ambiguity, and it can be cumbersome and challenging to model the “policy” entity in code.
通用语言要求每个术语都有单一含义,因此“政策”应该使用监管规则和保险合同这两个术语明确建模。
Ubiquitous language demands a single meaning for each term, so “policy” should be modeled explicitly using the two terms regulatory rule and insurance contract.
在通用语言中,两个术语不能互换使用。例如,许多系统使用术语user。然而,仔细检查领域专家的行话可能会发现用户和其他术语可以互换使用:例如,用户、访问者、管理员、帐户等。
Two terms cannot be used interchangeably in a ubiquitous language. For example, many systems use the term user. However, a careful examination of the domain experts’ lingo may reveal that user and other terms are used interchangeably: for example, user, visitor, administrator, account, etc.
同义词乍一看似乎无害。但是,在大多数情况下,它们表示不同的概念。在此示例中,访问者和帐户在技术上都是指系统的用户;然而,在大多数系统中,未注册和注册用户代表不同的角色并具有不同的行为。例如,“访问者”数据主要用于分析目的,而“账户”实际上使用系统及其功能。
Synonymous terms can seem harmless at first. However, in most cases, they denote different concepts. In this example, both visitor and account technically refer to the system’s users; however, in most systems, unregistered and registered users represent different roles and have different behaviors. For example, the “visitors” data is used mainly for analysis purposes, whereas “accounts” actually uses the system and its functionality.
最好在其特定上下文中明确使用每个术语。了解使用术语之间的差异可以构建更简单、更清晰的模型和业务领域实体的实现。
It is preferable to use each term explicitly in its specific context. Understanding the differences between the terms in use allows for building simpler and clearer models and implementations of the business domain’s entities.
现在让我们从不同的角度来看一下无处不在的语言:建模。
Now let’s look at the ubiquitous language from a different perspective: modeling.
模型是事物或事物的简化表示有意强调某些方面而忽略其他方面的现象。考虑到特定用途的抽象。
丽贝卡·沃夫斯-布鲁克
A model is a simplified representation of a thing or phenomenon that intentionally emphasizes certain aspects while ignoring others. Abstraction with a specific use in mind.
Rebecca Wirfs-Brock
模型不是现实世界的副本,而是帮助我们理解现实世界系统的人类构造。
A model is not a copy of the real world but a human construct that helps us make sense of real-world systems.
模型的典型示例是地图。任何地图都是模型,包括导航图、地形图、世界地图、地铁图等,如图2-3所示。
A canonical example of a model is a map. Any map is a model, including navigation maps, terrain maps, world maps, subway maps, and others, as shown in Figure 2-3.
这些地图都不代表我们星球的所有细节。相反,每张地图只包含足够的数据来支持其特定目的:它应该解决的问题。
None of these maps represents all the details of our planet. Instead, each map contains just enough data to support its particular purpose: the problem it is supposed to solve.
所有的模型都有一个目的,一个有效的模型只包含细节需要实现其目的。例如,您不会在世界地图上看到地铁站。另一方面,您不能使用地铁地图来估计距离。每张地图只包含它应该提供的信息。
All models have a purpose, and an effective model contains only the details needed to fulfill its purpose. For example, you won’t see subway stops on a world map. On the other hand, you cannot use a subway map to estimate distances. Each map contains just the information it is supposed to provide.
这一点值得重申:有用的模型不是现实世界的复制品。相反,模型旨在解决问题,并且它应该为此目的提供足够的信息。或者,正如统计学家 George Box 所说,“所有模型都是错误的,但有些是有用的。”
This point is worth reiterating: a useful model is not a copy of the real world. Instead, a model is intended to solve a problem, and it should provide just enough information for that purpose. Or, as statistician George Box put it, “All models are wrong, but some are useful.”
本质上,模型是一种抽象。抽象的概念使我们能够通过省略不必要的细节并仅留下解决手头问题所需的内容来处理复杂性。另一方面,无效的抽象会删除必要的信息或通过留下不需要的信息而产生噪音。正如 Edsger W. Dijkstra 在他的论文“The Humble Programmer”中指出的那样,4抽象的目的不是要模糊,而是要创建一个新的语义级别,在该级别中可以绝对精确。
In its essence, a model is an abstraction. The notion of abstraction allows us to handle complexity by omitting unnecessary details and leaving only what’s needed for solving the problem at hand. On the other hand, an ineffective abstraction removes necessary information or produces noise by leaving what’s not required. As noted by Edsger W. Dijkstra in his paper “The Humble Programmer,”4 the purpose of abstracting is not to be vague but to create a new semantic level in which one can be absolutely precise.
当培养一种无处不在的语言时,我们实际上是构建业务领域的模型。该模型应该捕捉领域专家的心智模型——他们关于业务如何运作以实现其功能的思维过程。该模型必须反映所涉及的业务实体及其行为、因果关系和不变量。
When cultivating a ubiquitous language, we are effectively building a model of the business domain. The model is supposed to capture the domain experts’ mental models—their thought processes about how the business works to implement its function. The model has to reflect the involved business entities and their behavior, cause and effect relationships, and invariants.
我们使用的无处不在的语言不应该涵盖领域的每一个可能的细节。这相当于让每个利益相关者都成为领域专家。相反,该模型应该只包含业务领域的足够方面,以便可以实现所需的系统;也就是说,解决软件旨在解决的特定问题。在接下来的章节中,您将看到无处不在的语言如何驱动低级设计和实现决策。
The ubiquitous language we use is not supposed to cover every possible detail of the domain. That would be equivalent to making every stakeholder a domain expert. Instead, the model is supposed to include just enough aspects of the business domain to make it possible to implement the required system; that is, to address the specific problem the software is intended to solve. In the following chapters, you will see how the ubiquitous language can drive low-level design and implementation decisions.
工程团队和领域专家之间的有效沟通至关重要。这种沟通的重要性随着业务领域的复杂性而增加。业务领域越复杂,就越难在代码中建模和实现其业务逻辑。即使对复杂的业务领域或其基本原则有轻微的误解,也会无意中导致容易出现严重错误的实现。验证业务领域的理解的唯一可靠方法是与领域专家交谈并使用他们理解的语言进行交流:业务语言。
Effective communication between engineering teams and domain experts is vital. The importance of this communication grows with the complexity of the business domain. The more complex the business domain is, the harder it is to model and implement its business logic in code. Even a slight misunderstanding of a complicated business domain, or its underlying principles, will inadvertently lead to an implementation prone to severe bugs. The only reliable way to verify a business domain’s understanding is to converse with domain experts and do it in the language they understand: the language of the business.
一种无处不在的语言的形成需要与它的自然持有者,领域专家。只有与实际领域专家的互动才能发现不准确、错误的假设或对业务领域的整体理解有缺陷。
Formulation of a ubiquitous language requires interaction with its natural holders, the domain experts. Only interactions with actual domain experts can uncover inaccuracies, wrong assumptions, or an overall flawed understanding of the business domain.
所有利益相关者都应在所有与项目相关的交流中始终使用通用语言来传播有关业务领域的知识并促进对业务领域的共同理解。该语言应该在整个项目中不断得到强化:需求、测试、文档,甚至源代码本身都应该使用这种语言。
All stakeholders should consistently use the ubiquitous language in all project-related communications to spread knowledge about and foster a shared understanding of the business domain. The language should be continuously reinforced throughout the project: requirements, tests, documentation, and even the source code itself should use this language.
最重要的是,培养一种无处不在的语言是一个持续的过程。它应该不断得到验证和发展。随着时间的推移,日常使用该语言将揭示对业务领域的更深刻见解。当这样的突破发生时,无处不在的语言必须进化以跟上新获得的领域知识的步伐。
Most importantly, cultivation of a ubiquitous language is an ongoing process. It should be constantly validated and evolved. Everyday use of the language will, over time, reveal deeper insights into the business domain. When such breakthroughs happen, the ubiquitous language must evolve to keep pace with the newly acquired domain knowledge.
There are tools and technologies that can alleviate the processes of capturing and managing a ubiquitous language.
例如,wiki 可以用作词汇表以捕获并记录无处不在的语言。这样的词汇表减轻了新团队成员的入职流程,因为它是获取有关业务领域术语的信息的首选之地。
For example, a wiki can be used as a glossary to capture and document the ubiquitous language. Such a glossary alleviates the onboarding process of new team members, as it serves as a go-to place for information about the business domain’s terminology.
共同努力维护词汇表很重要。当通用语言发生变化时,应鼓励所有团队成员继续更新词汇表。这与集中式方法相反,在这种方法中,只有团队领导或架构师负责维护词汇表。
It’s important to make glossary maintenance a shared effort. When a ubiquitous language is changed, all team members should be encouraged to go ahead and update the glossary. That’s contrary to a centralized approach, in which only team leaders or architects are in charge of maintaining the glossary.
尽管维护项目相关术语的词汇表有明显的优势,但它有一个固有的局限性。词汇表最适合“名词”:实体、流程、角色等的名称。尽管名词很重要,但捕捉行为也很关键。行为不仅仅是与名词相关联的动词列表,而是实际的业务逻辑及其规则、假设和不变量。这些概念更难记录在词汇表中。因此,词汇表最好与其他术语一起使用更适合捕捉行为的工具;例如,用例或Gherkin 测试。
Despite the obvious advantages of maintaining a glossary of project-related terminology, it has an inherent limitation. Glossaries work best for “nouns”: names of entities, processes, roles, and so on. Although nouns are important, capturing the behavior is crucial. The behavior is not a mere list of verbs associated with nouns, but the actual business logic, with its rules, assumptions, and invariants. Such concepts are much harder to document in a glossary. Hence, glossaries are best used in tandem with other tools that are better suited to capture the behavior; for example, use cases or Gherkin tests.
用Gherkin 语言编写的自动化测试不仅是捕获无处不在的语言的好工具,而且还可以作为弥合领域专家和软件工程师之间差距的附加工具。领域专家可以阅读测试并验证系统的预期行为。5例如看下面用Gherkin语言写的测试:
Automated tests written in the Gherkin language are not only great tools for capturing the ubiquitous language but also act as an additional tool for bridging the gap between domain experts and software engineers. Domain experts can read the tests and verify the system’s expected behavior.5 For example, see the following test written in the Gherkin language:
Scenario:Notify the agent about a new support caseGivenVincent Jules submits a new support case saying:"""I need help configuring AWS Infinidash"""Whenthe ticket is assigned to Mr. WolfThenthe agent receives a notification about the new ticket
Scenario:Notify the agent about a new support caseGivenVincent Jules submits a new support case saying:"""I need help configuring AWS Infinidash"""Whenthe ticket is assigned to Mr. WolfThenthe agent receives a notification about the new ticket
管理基于 Gherkin 的测试套件有时可能具有挑战性,尤其是在项目的早期阶段。但是,对于复杂的业务领域来说,这绝对是值得的。
Managing a Gherkin-based test suite can be challenging at times, especially at the early stages of a project. However, it is definitely worth it for complex business domains.
最后,甚至还有静态代码分析工具可以验证通用语言术语的使用情况。这种工具的一个著名示例是 NDepend。
Finally, there are even static code analysis tools that can verify the usage of a ubiquitous language’s terms. A notable example for such a tool is NDepend.
尽管这些工具很有用,但与在日常交互中实际使用无处不在的语言相比,它们是次要的。使用工具来支持管理无处不在的语言,但不要指望文档可以取代实际使用。正如敏捷宣言所说,“个人和交互高于流程和工具。”
While these tools are useful, they are secondary to the actual use of a ubiquitous language in day-to-day interactions. Use the tools to support the management of the ubiquitous language, but don’t expect the documentation to replace the actual usage. As the Agile Manifesto says, “Individuals and interactions over processes and tools.”
从理论上讲,培养一种无处不在的语言听起来像是简单、直接的过程。实际上,事实并非如此。收集领域知识的唯一可靠方法是与领域专家交谈。很多时候,最重要的知识是默会的。它没有记录或编纂,但只存在于领域专家的脑海中。访问它的唯一方法是提问。
In theory, cultivating a ubiquitous language sounds like a simple, straightforward process. In practice, it isn’t. The only reliable way to gather domain knowledge is to converse with domain experts. Quite often, the most important knowledge is tacit. It’s not documented or codified but resides only in the minds of domain experts. The only way to access it is to ask questions.
当您在此实践中获得经验时,您会经常注意到,此过程不仅涉及发现已经存在的知识,还涉及与领域专家一起共同创建模型。领域专家自身对业务领域的理解可能存在歧义甚至白点;例如,仅定义“快乐路径”场景,而不考虑挑战公认假设的边缘情况。此外,您可能会遇到缺乏明确定义的业务领域概念。询问有关业务领域的性质的问题通常会使这种隐含的冲突和白点变得明显。这对于核心子域尤其常见。在这种情况下,学习过程是相互的——你正在帮助领域专家更好地理解他们的领域。
As you gain experience in this practice, you will notice that frequently, this process involves not merely discovering knowledge that is already there, but rather co-creating the model in tandem with domain experts. There may be ambiguities and even white spots in domain experts’ own understanding of the business domain; for example, defining only the “happy path” scenarios but not considering edge cases that challenge the accepted assumptions. Furthermore, you may encounter business domain concepts that lack explicit definitions. Asking questions about the nature of the business domain often makes such implicit conflicts and white spots explicit. This is especially common for core subdomains. In such a case, the learning process is mutual—you are helping the domain experts better understand their field.
在将领域驱动设计实践引入棕地项目时,您会注意到已经形成了一种用于描述业务领域的语言,并且利益相关者使用它。但是,由于 DDD 原则不驱动该语言,因此它不一定有效地反映业务领域。例如,它可能使用技术术语,例如数据库表名。更改已在组织中使用的语言并不容易。在这种情况下,必不可少的工具是耐心。您需要确保在易于控制的地方使用正确的语言:在文档和源代码中。
When introducing domain-driven design practices to a brownfield project, you will notice that there is already a formed language for describing the business domain, and that the stakeholders use it. However, since DDD principles do not drive that language, it won’t necessarily reflect the business domain effectively. For example, it may use technical terms, such as database table names. Changing a language that is already being used in an organization is not easy. The essential tool in such a situation is patience. You need to make sure the correct language is used where it’s easy to control it: in the documentation and source code.
最后,我在会议上经常被问到的关于无处不在的语言的问题是,如果公司不在英语国家,我们应该使用什么语言。我的建议是至少使用英语名词来命名业务领域的实体。这将减少在代码中使用相同的术语。
Finally, the question about the ubiquitous language that I am asked often at conferences is what language should we use if the company is not in an English-speaking country. My advice is to at least use English nouns for naming the business domain’s entities. This will alleviate using the same terminology in code.
有效的沟通和知识共享对于成功的软件项目至关重要。软件工程师必须了解业务领域才能设计和构建软件解决方案。
Effective communication and knowledge sharing are crucial for a successful software project. Software engineers have to understand the business domain in order to design and build a software solution.
领域驱动设计的通用语言是弥合领域专家和软件工程师之间知识鸿沟的有效工具。它通过培养一种共享语言来促进沟通和知识共享,这种语言可以在整个项目中被所有利益相关者使用:在对话、文档、测试、图表、源代码等中。
Domain-driven design’s ubiquitous language is an effective tool for bridging the knowledge gap between domain experts and software engineers. It fosters communication and knowledge sharing by cultivating a shared language that can be used by all the stakeholders throughout the project: in conversations, documentation, tests, diagrams, source code, and so on.
为了确保有效的沟通,无处不在的语言必须消除歧义和隐含的假设。一种语言的所有术语都必须是一致的——没有模棱两可的术语,也没有同义词。
To ensure effective communication, the ubiquitous language has to eliminate ambiguities and implicit assumptions. All of a language’s terms have to be consistent—no ambiguous terms and no synonymous terms.
培养一种无处不在的语言是一个持续的过程。随着项目的发展,将发现更多的领域知识。将这种见解反映在无处不在的语言中非常重要。
Cultivating a ubiquitous language is a continuous process. As the project evolves, more domain knowledge will be discovered. It’s important for such insights to be reflected in the ubiquitous language.
基于 wiki 的词汇表和 Gherkin 测试等工具可以大大减轻记录和维护无处不在的语言的过程。然而,有效的通用语言的主要先决条件是使用:必须在所有与项目相关的通信中一致地使用该语言。
Tools such as wiki-based glossaries and Gherkin tests can greatly alleviate the process of documenting and maintaining a ubiquitous language. However, the main prerequisite for an effective ubiquitous language is usage: the language has to be used consistently in all project-related communications.
谁应该能够为无处不在的语言的定义做出贡献?
领域专家
软件工程师
终端用户
项目的所有利益相关者
Who should be able to contribute to the definition of a ubiquitous language?
Domain experts
Software engineers
End users
All of the project’s stakeholders
应该在哪里使用无处不在的语言?
面对面对话
文档
代码
上述所有的
Where should a ubiquitous language be used?
In-person conversations
Documentation
Code
All of the above
请查看前言中虚构的 WolfDesk 公司的描述。您可以在描述中发现哪些业务领域术语?
Please review the description of the fictional WolfDesk company in the Preface. What business domain terminology can you spot in the description?
考虑您目前正在从事或过去从事的软件项目:
尝试提出您可以在与领域专家对话时使用的业务领域概念。
尝试识别不一致术语的示例:具有不同含义或由不同术语表示的相同概念的业务领域概念。
您是否遇到过因沟通不畅而导致软件开发效率低下的情况?
Consider a software project you are working on at the moment or worked on in the past:
Try to come up with concepts of the business domain that you could use in conversations with domain experts.
Try to identify examples of inconsistent terms: business domain concepts that have either different meanings or identical concepts represented by different terms.
Have you encountered software development inefficiencies that resulted from poor communication?
假设您正在处理一个项目,并且您注意到来自不同组织单位的领域专家使用相同的术语(例如policy)来描述业务领域的不相关概念。
由此产生的无处不在的语言基于领域专家的心智模型,但未能满足术语具有单一含义的要求。
在继续下一章之前,您将如何解决这样的难题?
Assume you are working on a project and you notice that domain experts from different organizational units use the same term, for example, policy, to describe unrelated concepts of the business domain.
The resultant ubiquitous language is based on domain experts’ mental models but fails to fulfill the requirement of a term having a single meaning.
Before you continue to the next chapter, how would you address such a conundrum?
1个布兰多里尼,阿尔贝托。(nd). 介绍 EventStorming。精益酒吧。
1 Brandolini, Alberto. (n.d.). Introducing EventStorming. Leanpub.
2个Sudhakar,Goparaju Purna。(2012)。“软件项目的关键成功因素模型。” 企业信息管理杂志,25 (6),537–558。
2 Sudhakar, Goparaju Purna. (2012). “A Model of Critical Success Factors for Software Projects.” Journal of Enterprise Information Management, 25(6), 537–558.
3个玩家排成一排,第一个玩家想出一条消息,然后把它耳语到第二个玩家的耳朵里。第二个玩家将消息重复给第三个玩家,依此类推。最后一位玩家向整个小组宣布他们听到的消息。然后第一个玩家将原始消息与最终版本进行比较。尽管目标是传达相同的消息,但它通常会出现乱码,最后一位玩家收到的消息与原始消息有很大不同。
3 Players form a line, and the first player comes up with a message and whispers it into the ear of the second player. The second player repeats the message to the third player, and so on. The last player announces the message they heard to the entire group. The first player then compares the original message with the final version. Although the objective is to communicate the same message, it usually gets garbled and the last player receives a message that is significantly different from the original one.
4个Edsger W. Dijkstra,“谦逊的程序员”。
4 Edsger W. Dijkstra, “The Humble Programmer”.
5个但是请不要陷入认为领域专家会编写 Gherkin 测试的陷阱。
5 But please don’t fall into the trap of thinking that domain experts will write Gherkin tests.
正如您在上一章中看到的那样,确保项目成功开发一种通用语言非常重要,所有利益相关者(从软件工程师到领域专家)都可以使用这种语言进行交流。该语言应该反映领域专家对业务领域内部运作和基本原则的心智模型。
As you saw in the previous chapter, to ensure a project’s success it’s crucial that you develop a ubiquitous language that can be used for communication by all stakeholders, from software engineers to domain experts. The language should reflect the domain experts’ mental models of the business domain’s inner workings and underlying principles.
由于我们的目标是使用无处不在的语言来驱动软件设计决策,因此语言必须清晰一致。它应该没有歧义、隐含的假设和无关的细节。然而,在组织范围内,领域专家的心智模型本身可能不一致。不同的领域专家可以使用同一业务领域的不同模型。让我们看一个例子。
Since our goal is to use ubiquitous language to drive software design decisions, the language must be clear and consistent. It should be free of ambiguity, implicit assumptions, and extraneous details. However, on an organizational scale, the domain experts’ mental models can be inconsistent themselves. Different domain experts can use different models of the same business domain. Let’s take a look at an example.
让我们回到第 2 章中电话营销公司的例子。公司的营销部门通过在线广告产生销售线索。它的销售部门负责招揽潜在客户购买它的产品或服务,如图3-1所示的链条。
Let’s go back to the example of a telemarketing company from Chapter 2. The company’s marketing department generates leads through online advertisements. Its sales department is in charge of soliciting prospective customers to buy its products or services, a chain that is shown in Figure 3-1.
对领域专家语言的检查揭示了一个奇特的观察结果。领导一词在营销和销售部门有不同的含义:
An examination of the domain experts’ language reveals a peculiar observation. The term lead has different meanings in the marketing and sales departments:
在这家电话营销公司的案例中,我们如何制定一种无处不在的语言?
How do we formulate a ubiquitous language in the case of this telemarketing company?
一方面,我们知道无处不在的语言必须是一致的——每个术语都应该有一个含义。另一方面,我们知道无处不在的语言必须反映领域专家的心智模型。在这种情况下,“领导”的心智模型在销售和市场部门的领域专家之间是不一致的。
On the one hand, we know the ubiquitous language has to be consistent—each term should have one meaning. On the other hand, we know the ubiquitous language has to reflect the domain experts’ mental models. In this case, the mental model of the “lead” is inconsistent among the domain experts in the sales and marketing departments.
这种歧义在人与人之间的交流中并没有带来太大的挑战。事实上,来自不同部门的人之间的沟通可能更具挑战性,但人类很容易从交互的上下文中推断出确切的含义。
This ambiguity doesn’t present that much of a challenge in person-to-person communications. Indeed, communication can be more challenging among people from different departments, but it’s easy enough for humans to infer the exact meaning from the interaction’s context.
然而,在软件中表示业务领域的这种不同模型更加困难。源代码不能很好地处理歧义。如果我们要将销售部门的复杂模型引入市场营销,它会在不需要的地方引入复杂性——比营销人员优化广告活动所需的细节和行为要多得多。但是,如果我们试图按照营销世界观来简化销售模型,那么它不符合销售子域的需要,因为它对于管理和优化销售流程来说过于简单。在第一种情况下我们会有一个过度设计的解决方案,而在第二种情况下我们会有一个设计不足的解决方案。
However, it is more difficult to represent such a divergent model of the business domain in software. Source code doesn’t cope well with ambiguity. If we were to bring the sales department’s complicated model into marketing, it would introduce complexity where it’s not needed— far more detail and behavior than marketing people need for optimizing advertising campaigns. But if we were to try to simplify the sales model according to the marketing world view, it wouldn’t fit the sales subdomain’s needs, because it’s too simplistic for managing and optimizing the sales process. We’d have an overengineered solution in the first case and an under-engineered one in the second.
我们如何解决这个第 22 条军规?
How do we solve this catch-22?
这个问题的传统解决方案是设计一个可以用于所有类型问题的单一模型。这样的模型会产生跨越整个办公室墙壁的巨大实体关系图 (ERD)。图 3-2是一个有效的模型吗?
The traditional solution to this problem is to design a single model that can be used for all kinds of problems. Such models result in enormous entity relationship diagrams (ERDs) spanning whole office walls. Is Figure 3-2 an effective model?
俗话说,“万事通,万事通”。这样的模型应该适用于一切,但最终却一无所获。无论您做什么,您总是面临着复杂性:过滤掉无关细节的复杂性,找到您确实需要的复杂性,最重要的是,保持数据处于一致状态的复杂性。
As the saying goes, “jack of all trades, master of none.” Such models are supposed to be suitable for everything but eventually are effective for nothing. No matter what you do, you are always facing complexity: the complexity of filtering out extraneous details, the complexity of finding what you do need, and most importantly, the complexity of keeping the data in a consistent state.
另一种解决方案是在有问题的术语前面加上上下文定义:“营销线索”和“销售线索”。这将允许在代码中实现这两个模型。然而,这种方法有两个主要缺点。首先,它会引起认知负荷。什么时候应该使用每个模型?冲突模型的实现越接近,就越容易出错。其次,模型的实现不会与无处不在的语言保持一致。没有人会在对话中使用前缀。人们不需要这些额外的信息;他们可以依赖对话的上下文。
Another solution would be to prefix the problematic term with a definition of the context: “marketing lead” and “sales lead.” That would allow the implementation of the two models in code. However, this approach has two main disadvantages. First, it induces cognitive load. When should each model be used? The closer the implementations of the conflicting models are, the easier it is to make a mistake. Second, the implementation of the model won’t be aligned with the ubiquitous language. No one would use the prefixes in conversations. People don’t need this extra information; they can rely on the conversation’s context.
让我们转向用于处理此类场景的领域驱动设计模式:限界上下文模式。
Let’s turn to the domain-driven design pattern for tackling such scenarios: the bounded context pattern.
领域驱动设计中的解决方案很简单:划分将无处不在的语言分解成多种较小的语言,然后将每一种语言分配给可以应用它的显式上下文:它的有界上下文。
The solution in domain-driven design is trivial: divide the ubiquitous language into multiple smaller languages, then assign each one to the explicit context in which it can be applied: its bounded context.
在前面的示例中,我们可以确定两个限界上下文:营销和销售。术语lead存在于两个限界上下文中,如图3-3所示。只要它在每个有界上下文中具有单一含义,每个细粒度的无处不在的语言都是一致的,并遵循领域专家的心智模型。
In the preceding example, we can identify two bounded contexts: marketing and sales. The term lead exists in both bounded contexts, as shown in Figure 3-3. As long as it bears a single meaning in each bounded context, each fine-grained ubiquitous language is consistent and follows the domain experts’ mental models.
从某种意义上说,术语冲突和隐含上下文是任何体面规模的企业的固有部分。使用限界上下文模式,上下文被建模为业务领域的显式和不可分割的部分。
In a sense, terminology conflicts and implicit contexts are an inherent part of any decent-sized business. With the bounded context pattern, the contexts are modeled as an explicit and integral part of the business domain.
正如我们在上一章中讨论的那样,模型不是现实世界的副本,而是一种帮助我们理解复杂系统的结构。它应该解决的问题是模型的固有部分——它的目的。模型不能没有边界存在;它将扩展成为现实世界的副本。这使得定义模型的边界——它的有界上下文——成为建模过程的一个固有部分。
As we discussed in the previous chapter, a model is not a copy of the real world but a construct that helps us make sense of a complex system. The problem it is supposed to solve is an inherent part of a model—its purpose. A model cannot exist without a boundary; it will expand to become a copy of the real world. That makes defining a model’s boundary—its bounded contexts—an intrinsic part of the modeling process.
让我们回到地图作为模型的例子。我们看到每张地图都有其特定的上下文——航空、航海、地形、地铁等等。地图只有在其特定用途范围内才有用和一致。
Let’s go back to the example of maps as models. We saw that each map has its specific context—aerial, nautical, terrain, subway, and so on. A map is useful and consistent only within the scope of its specific purpose.
正如地铁地图对航海导航毫无用处一样,一种在一个有界上下文中无处不在的语言可能与另一个有界上下文的范围完全无关。限界上下文定义了一种通用语言及其代表的模型的适用性。它们允许根据不同的问题域定义不同的模型。换句话说,有界上下文是无处不在的语言的一致性边界。一种语言的术语、原则和业务规则仅在其限界上下文中是一致的。
Just as a subway map is useless for nautical navigation, a ubiquitous language in one bounded context can be completely irrelevant to the scope of another bounded context. Bounded contexts define the applicability of a ubiquitous language and of the model it represents. They allow defining distinct models according to different problem domains. In other words, bounded contexts are the consistency boundaries of ubiquitous languages. A language’s terminology, principles, and business rules are only consistent inside its bounded context.
限界上下文使我们能够完成通用语言的定义。一种无处不在的语言并不是“无处不在”的,因为它应该在整个组织中“无处不在”地使用和应用。无处不在的语言不是通用的。
Bounded contexts allow us to complete the definition of a ubiquitous language. A ubiquitous language is not “ubiquitous” in the sense that it should be used and applied “ubiquitously” throughout the organization. A ubiquitous language is not universal.
相反,无处不在的语言仅在其有界上下文的边界内无处不在。该语言专注于仅描述限界上下文所包含的模型。由于模型没有它应该解决的问题就不可能存在,所以如果没有明确的适用性上下文,就不能定义或使用无处不在的语言。
Instead, a ubiquitous language is ubiquitous only in the boundaries of its bounded context. The language is focused on describing only the model that is encompassed by the bounded context. As a model cannot exist without a problem it is supposed to address, a ubiquitous language cannot be defined or used without an explicit context of its applicability.
本章开头的例子展示了一个业务领域的固有边界。不同领域的专家对同一业务实体持有相互矛盾的心智模型。为了对业务领域进行建模,我们必须划分模型并为每个细粒度模型定义严格的适用性上下文——它的有界上下文。
The example at the beginning of the chapter demonstrated an inherent boundary of the business domain. Different domain experts held conflicting mental models of the same business entity. To model the business domain, we had to divide the model and define a strict applicability context for each fine-grained model—its bounded context.
无处不在的语言的一致性仅有助于识别该语言的最宽边界。它不能再大了,因为那样就会出现不一致的模型和术语。然而,我们仍然可以将模型进一步分解为更小的有界上下文,如图3-4所示。
The consistency of the ubiquitous language only helps to identify the widest boundary of that language. It cannot be any larger, because then there will be inconsistent models and terminology. However, we can still further decompose the models into even smaller bounded contexts, as shown in Figure 3-4.
定义无处不在的语言的范围——它的限界上下文——是一个战略设计决策。边界可以很宽,遵循业务领域的固有上下文,也可以很窄,进一步将业务领域划分为更小的问题域。
Defining the scope of a ubiquitous language—its bounded context—is a strategic design decision. Boundaries can be wide, following the business domain’s inherent contexts, or narrow, further dividing the business domain into smaller problem domains.
限界上下文的大小本身并不是决定性因素。模型不一定是大的或小的。模型需要有用。通用语言的边界越广,就越难保持一致。将大型无处不在的语言划分为更小、更易于管理的问题域可能是有益的,但争取小的有界上下文也会适得其反。它们越小,设计带来的集成开销就越大。
A bounded context’s size, by itself, is not a deciding factor. Models shouldn’t necessarily be big or small. Models need to be useful. The wider the boundary of the ubiquitous language is, the harder it is to keep it consistent. It may be beneficial to divide a large ubiquitous language into smaller, more manageable problem domains, but striving for small bounded contexts can backfire too. The smaller they are, the more integration overhead the design induces.
因此,边界上下文的大小取决于具体的问题领域。有时,使用宽边界会更清晰,而在其他时候,进一步分解会更有意义。
Hence, the decision for how big your bounded contexts should depend on the specific problem domain. Sometimes, using a wide boundary will be clearer, while at other times, decomposing it further will make more sense.
从较大的限界上下文中提取较细粒度的上下文的原因包括组建新的软件工程团队或解决系统的一些非功能性需求;例如,当您需要分离最初驻留在单个有界上下文中的某些组件的开发生命周期时。提取一个功能的另一个常见原因是能够独立于限界上下文的其余功能对其进行扩展。
The reasons for extracting finer-grained bounded contexts out of a larger one include constituting new software engineering teams or addressing some of the system’s nonfunctional requirements; for example, when you need to separate the development lifecycles of some of the components originally residing in a single bounded context. Another common reason for extracting one functionality is the ability to scale it independently from the rest of the bounded context’s functionalities.
因此,请保持您的模型有用,并使限界上下文的大小与您的业务需求和组织约束保持一致。需要注意的一件事是将连贯的功能拆分为多个有界上下文。这种划分将阻碍独立发展每个上下文的能力。相反,相同的业务需求和变更将同时影响限界上下文并需要同时部署变更。为避免这种无效的分解,请使用我们在第 1 章中讨论的经验法则来查找子域:识别对相同数据进行操作的连贯用例集,并避免将它们分解为多个有界上下文。
Therefore, keep your models useful and align the bounded contexts’ sizes with your business needs and organizational constraints. One thing to beware of is splitting a coherent functionality into multiple bounded contexts. Such division will hinder the ability to evolve each context independently. Instead, the same business requirements and changes will simultaneously affect the bounded contexts and require simultaneous deployment of the changes. To avoid such ineffective decomposition, use the rule of thumb we discussed in Chapter 1 to find subdomains: identify sets of coherent use cases that operate on the same data and avoid decomposing them into multiple bounded contexts.
我们将在第 8章和第 10章中进一步讨论持续优化限界上下文边界的主题.
We’ll discuss the topic of continuously optimizing the bounded contexts’ boundaries further in Chapters 8 and 10.
在第 2 章中,我们看到业务领域由多个子域组成。到目前为止,在本章中,我们探讨了将业务域分解为一组细粒度问题域或限界上下文的概念。乍一看,这两种分解业务领域的方法似乎是多余的。然而,事实并非如此。让我们来看看为什么我们需要两个边界。
In Chapter 2, we saw that a business domain consists of multiple subdomains. So far in this chapter, we explored the notion of decomposing a business domain into a set of fine-grained problem domains or bounded contexts. At first, the two methods of decomposing business domains might seem redundant. However, that’s not the case. Let’s examine why we need both boundaries.
要理解一个公司的经营战略,我们必须分析它的业务领域。根据领域驱动设计方法,分析阶段涉及识别不同的子域(核心、支持和通用)。这就是组织运作和规划其竞争战略的方式。
To comprehend a company’s business strategy, we have to analyze its business domain. According to domain-driven design methodology, the analysis phase involves identifying the different subdomains (core, supporting, and generic). That’s how the organization works and plans its competitive strategy.
正如您在第 1 章中了解到的,子域类似于一组相互关联的用例。用例由业务领域和系统需求定义。作为软件工程师,我们不定义需求;这是企业的责任。相反,我们正在分析业务领域以识别子领域。
As you learned in Chapter 1, a subdomain resembles a set of interrelated use cases. The use cases are defined by the business domain and the system’s requirements. As software engineers, we do not define the requirements; that’s the responsibility of the business. Instead, we are analyzing the business domain to identify the subdomains.
另一方面,有界上下文是设计出来的。选择模型的边界是一项战略设计决策。我们决定如何将业务领域划分为更小的、可管理的问题领域。
Bounded contexts, on the other hand, are designed. Choosing models’ boundaries is a strategic design decision. We decide how to divide the business domain into smaller, manageable problem domains.
从理论上讲,虽然不切实际,但单个模型可以跨越整个业务领域。这种策略适用于小型系统,如图3-5所示。
Theoretically, though impractically, a single model could span the entire business domain. This strategy could work for a small system, as shown in Figure 3-5.
当出现模型冲突时,我们可以遵循领域专家的心智模型,将系统分解为限界上下文,如图3-6所示。
When conflicting models arise, we can follow the domain experts’ mental models and decompose the systems into bounded contexts, as shown in Figure 3-6.
如果模型仍然很大且难以维护,我们可以将它们分解成更小的有界上下文;例如,通过为每个子域设置一个限界上下文,如图3-7所示。
If the models are still large and hard to maintain, we can decompose them into even smaller bounded contexts; for example, by having a bounded context for each subdomain, as shown in Figure 3-7.
无论哪种方式,这都是一个设计决定。我们将这些边界设计为解决方案的一部分。
Either way, this is a design decision. We design those boundaries as a part of the solution.
在某些情况下,在有界上下文和子域之间建立一对一的关系是完全合理的。然而,在其他情况下,不同的分解策略可能更合适。
Having a one-to-one relationship between bounded contexts and subdomains can be perfectly reasonable in some scenarios. In others, however, different decomposition strategies can be more suitable.
重要的是要记住,子域是被发现的,限界上下文是设计的。1子域由业务策略定义。但是,我们可以设计软件解决方案及其限界上下文来解决特定项目的上下文和约束。
It’s crucial to remember that subdomains are discovered and bounded contexts are designed.1 The subdomains are defined by the business strategy. However, we can design the software solution and its bounded contexts to address the specific project’s context and constraints.
最后,正如您在第 1 章中了解到的,模型旨在解决特定问题。在某些情况下,同时使用同一概念的多个模型来解决不同的问题可能是有益的。由于不同类型的地图提供了关于我们星球的不同类型的信息,因此使用同一子域的不同模型来解决不同的问题可能是合理的。将设计限制为限界上下文之间的一对一关系会抑制这种灵活性,并迫使我们在其限界上下文中使用子域的单一模型。
Finally, as you learned in Chapter 1, a model is intended to solve a specific problem. In some cases, it can be beneficial to use multiple models of the same concept simultaneously to solve different problems. As different types of maps provide different types of information about our planet, it may be reasonable to use different models of the same subdomain to solve different problems. Limiting the design to one-to-one relationships between bounded contexts would inhibit this flexibility and force us to use a single model of a subdomain in its bounded context.
正如 Ruth Malan 所说,建筑设计本质上是关于边界的:
As Ruth Malan says, architectural design is inherently about boundaries:
架构设计是系统设计。系统设 它重塑外部事物,就像它塑造内部事物一样。2个
Architectural design is system design. System design is contextual design—it is inherently about boundaries (what’s in, what’s out, what spans, what moves between), and about trade-offs. It reshapes what is outside, just as it shapes what is inside.2
限界上下文模式是用于定义物理和所有权边界的领域驱动设计工具。
The bounded context pattern is the domain-driven design tool for defining physical and ownership boundaries.
有界上下文不仅用作模型边界,而且作为实现它们的系统的物理边界。每个限界上下文都应该作为一个单独的服务/项目来实现,这意味着它的实现、发展和版本控制独立于其他限界上下文。
Bounded contexts serve not only as model boundaries but also as physical boundaries of the systems implementing them. Each bounded context should be implemented as an individual service/project, meaning it is implemented, evolved, and versioned independently of other bounded contexts.
有界上下文之间清晰的物理边界使我们能够使用最适合其需求的技术堆栈来实现每个有界上下文。
Clear physical boundaries between bounded contexts allow us to implement each bounded context with the technology stack that best fits its needs.
正如我们之前讨论的,有界上下文可以包含多个子域。在这种情况下,限界上下文是一个物理边界,而它的每个子域都是一个逻辑边界。逻辑边界在不同的编程语言中有不同的名称:命名空间、模块或包。
As we discussed earlier, a bounded context can contain multiple subdomains. In such a case, the bounded context is a physical boundary, while each of its subdomains is a logical boundary. Logical boundaries bear different names in different programming languages: namespaces, modules, or packages.
研究表明,好的围栏确实能成为好邻居。在软件项目中,我们可以利用模型边界(有界上下文)实现团队的和平共处。团队之间的工作分工是另一个可以使用限界上下文模式做出的战略决策。
Studies show that good fences do indeed make good neighbors. In software projects, we can leverage model boundaries—bounded contexts—for the peaceful coexistence of teams. The division of work between teams is another strategic decision that can be made using the bounded context pattern.
限界上下文应该只由一个团队实施、发展和维护。没有两个团队可以在同一个限界上下文中工作。这种隔离消除了团队可能对彼此的模型做出的隐含假设。相反,他们必须明确地定义通信协议来集成他们的模型和系统。
A bounded context should be implemented, evolved, and maintained by one team only. No two teams can work on the same bounded context. This segregation eliminates implicit assumptions that teams might make about one another’s models. Instead, they have to define communication protocols for integrating their models and systems explicitly.
重要的是要注意团队和限界上下文之间的关系是单向的:限界上下文应该只由一个团队拥有。然而,一个团队可以拥有多个限界上下文,如图3-8所示。
It’s important to note that the relationship between teams and bounded contexts is one-directional: a bounded context should be owned by only one team. However, a single team can own multiple bounded contexts, as Figure 3-8 illustrates.
在我的一个领域驱动设计课程中,一位参与者曾经注意:“你说 DDD 是关于使软件设计与业务领域保持一致。但是现实生活中的限界上下文在哪里呢?业务领域中没有限界上下文。”
In one of my domain driven-design classes, a participant once noted: “You said that DDD is about aligning software design with business domains. But where are the bounded contexts in real life? There are no bounded contexts in business domains.”
事实上,限界上下文不像业务领域和子领域那样明显,但它们确实存在,就像领域专家的心智模型一样。您只需要了解领域专家如何看待不同的业务实体和流程。
Indeed, bounded contexts are not as evident as business domains and subdomains, but they are there, as domain experts’ mental models are. You just have to be conscious about how domain experts think about the different business entities and processes.
我想通过讨论示例来结束本章,这些示例表明不仅在我们对软件中的业务领域建模时存在限界上下文,而且在不同上下文中使用不同模型的概念在一般生活中也很普遍。
I want to close this chapter by discussing examples demonstrating that not only are bounded contexts there when we are modeling business domains in software, but the notion of using different models in different contexts is widespread in life in general.
可以说领域驱动设计的限界上下文是基于语义域的词典编纂概念。语义域被定义为意义区域和用来谈论它的词语。例如,单词monitor、port和processor在软件和硬件工程语义域中具有不同的含义。
It can be said that domain-driven design’s bounded contexts are based on the lexicographical notion of semantic domains. A semantic domain is defined as an area of meaning and the words used to talk about it. For example, the words monitor, port, and processor have different meanings in the software and hardware engineering semantic domains.
不同语义域的一个相当特殊的例子是tomato一词的含义。
A rather peculiar example of different semantic domains is the meaning of the word tomato.
根据植物学定义,果实是植物传播种子的方式。果实应从植物的花中长出,并至少结出一粒种子。另一方面,蔬菜是一个通用术语,包括植物的所有其他可食用部分:根、茎和叶。根据这个定义,西红柿是一种水果。
According to the botanic definition, a fruit is the plant’s way of spreading its seeds. A fruit should grow from the plant’s flower, and bear at least one seed. A vegetable, on the other hand, is a general term encompassing all other edible parts of a plant: roots, stems, and leaves. Based on this definition, the tomato is a fruit.
然而,这个定义在烹饪艺术的背景下几乎没有用处。在这种情况下,水果和蔬菜是根据它们的风味特征来定义的。水果质地柔软,或甜或酸,可以生吃,而蔬菜质地较硬,味道较淡,通常需要烹饪。根据这个定义,西红柿是一种蔬菜。
That definition, however, is of little use in the context of the culinary arts. In this context, fruits and vegetables are defined based on their flavor profiles. A fruit has a soft texture, is either sweet or sour, and can be enjoyed in its raw form, whereas a vegetable has a tougher texture, tastes blander, and often requires cooking. According to this definition, the tomato is a vegetable.
因此,在植物学的有限语境中,西红柿是一种水果,而在烹饪艺术的有限语境中,它是一种蔬菜。但这还不是全部。
Hence, in the bounded context of botany, the tomato is a fruit, while in the bounded context of the culinary arts, it’s a vegetable. But that’s not all.
1883 年,美国对进口蔬菜征收 10% 的税,但不征收水果税。番茄作为水果的植物学定义允许将番茄进口到美国而无需支付进口税。为填补漏洞,美国最高法院于 1893 年做出将番茄归类为蔬菜的决定。因此,在税收的有界背景下,西红柿是一种蔬菜。
In 1883 the United States established a 10% tax on imported vegetables, but not fruits. The botanic definition of the tomato as a fruit allowed the importation of tomatoes to the United States without paying the import tax. To close the loophole, in 1893 the United States Supreme Court made the decision to classify the tomato as a vegetable. Therefore, in the bounded context of taxation, the tomato is a vegetable.
此外,正如我的朋友 Romeu Moura 所说,在戏剧表演的有限背景下,番茄是一种反馈机制。
Furthermore, as my friend Romeu Moura says, in the bounded context of theatrical performances, the tomato is a feedback mechanism.
正如历史学家 Yuval Noah Harari 所说,“科学家们普遍认为没有理论是 100% 正确的。因此,知识的真正考验不是真理,而是实用性。” 换句话说,没有任何科学理论在所有情况下都是正确的。不同的理论在不同的情况下是有用的。
As historian Yuval Noah Harari puts it, “Scientists generally agree that no theory is 100 percent correct. Thus, the real test of knowledge is not the truth, but utility.” In other words, no scientific theory is correct in all cases. Different theories are useful in different contexts.
这个概念可以通过艾萨克牛顿爵士和阿尔伯特爱因斯坦引入的不同引力模型来证明。根据牛顿运动定律,空间和时间是绝对的。它们是物体运动发生的舞台。在爱因斯坦的相对论中,空间和时间不再是绝对的,而是对不同的观察者不同。
This notion can be demonstrated by the different models of gravity introduced by Sir Isaac Newton and Albert Einstein. According to Newton’s laws of motion, space and time are absolute. They are the stage on which the motion of objects happens. In Einstein’s theory of relativity, space and time are no longer absolute but different for different observers.
尽管这两个模型可以被看作是矛盾的,但它们在它们合适的(有界的)上下文中都是有用的。
Even though the two models can be seen as contradictory, both are useful in their suitable (bounded) contexts.
最后,让我们看一个更接地气的现实生活中的例子有界上下文。你在图 3-9中看到了什么?
Finally, let’s see a more earthbound example of real-life bounded contexts. What do you see in Figure 3-9?
它只是一张纸板吗?不,它是一个模型。这是西门子KG86NAI31L冰箱的型号。如果你查一下,你可能会说这块硬纸板看起来一点也不像那个冰箱。它没有门,甚至它的颜色也不一样。
Is it just a piece of cardboard? No, it’s a model. It’s a model of the Siemens KG86NAI31L refrigerator. If you look it up, you may say the piece of cardboard doesn’t look anything like that fridge. It has no doors, and even its color is different.
虽然那是真的,但这无关紧要。正如我们所讨论的,模型不应该复制真实世界的实体。相反,它应该有一个目的——一个它应该解决的问题。因此,关于纸板的正确问题是,这个模型解决了什么问题?
Although that’s true, it’s not relevant. As we’ve discussed, a model is not supposed to copy a real-world entity. Instead, it should have a purpose—a problem it is supposed to solve. Hence, the correct question to ask about the cardboard is, what problem does this model solve?
在我们的公寓里,我们没有标准的厨房入口。纸板被精确地切割成冰箱宽度和深度的尺寸。它解决的问题是检查冰箱是否可以穿过厨房的门(见图3-10)。
In our apartment, we do not have a standard entry into the kitchen. The cardboard was cut precisely to the size of the fridge’s width and depth. The problem it solves is checking whether the refrigerator can fit through the kitchen door (see Figure 3-10).
尽管硬纸板看起来一点也不像冰箱,但事实证明,当我们不得不决定是购买这种型号还是选择较小的型号时,它非常有用。同样,所有模型都是错误的,但有些是有用的。构建冰箱的 3D 模型绝对是一个有趣的项目。但它会比硬纸板更有效地解决问题吗?不。如果纸板适合,3D 模型也适合,反之亦然。用软件工程术语来说,构建冰箱的 3D 模型属于严重的过度工程。
Despite the cardboard not looking anything like the fridge, it proved extremely useful when we had to decide whether to buy this model or opt for a smaller one. Again, all models are wrong, but some are useful. Building a 3D model of the fridge would definitely be a fun project. But would it solve the problem any more efficiently than the cardboard? No. If the cardboard fits, the 3D model would fit as well, and vice versa. In software engineering terms, building a 3D model of the fridge would be gross overengineering.
但是冰箱的高度呢?如果底座适合,但它太高而无法放入门口怎么办?这可以证明将冰箱的 3D 模型粘合在一起是合理的吗?不会。使用简单的卷尺检查门口的高度可以更快更轻松地解决问题。在这种情况下,卷尺是什么?另一个简单的模型。
But what about the refrigerator’s height? What if the base fits, but it’s too tall to fit in the doorway? Would that justify gluing together a 3D model of the fridge? No. The problem can be solved much more quickly and easily by using a simple tape measure to check the doorway’s height. What is a tape measure in this case? Another simple model.
因此,我们最终得到了同一台冰箱的两种型号。使用两个模型,每个模型都针对其特定任务进行了优化,反映了 DDD 方法对业务领域进行建模。每个模型都有其严格的有界上下文:纸板验证冰箱底座是否可以穿过厨房的入口,卷尺验证它不是太高。模型应该忽略与手头任务无关的无关信息。此外,如果多个更简单的模型可以有效地单独解决每个问题,则无需设计复杂的万事通模型。
So, we ended up with two models of the same fridge. Using two models, each optimized for its specific task, reflects the DDD approach to modeling business domains. Each model has its strict bounded context: the cardboard verifying that the refrigerator’s base can make it through the kitchen’s entry, and the tape measure verifying that it’s not too tall. A model should omit the extraneous information irrelevant to the task at hand. Also, there’s no need to design a complex jack-of-all-trades model if multiple, much simpler models can effectively solve each problem individually.
在我在 Twitter 上发布这个故事几天后,我收到了一条回复,说我可以不用摆弄纸板,而是使用带有 LiDAR 扫描仪和增强现实 (AR) 应用程序的手机。让我们从领域驱动设计的角度来分析这个建议。
A few days after I published this story on Twitter, I received a reply saying that instead of fiddling with cardboard, I could have just used a mobile phone with a LiDAR scanner and an augmented reality (AR) application. Let’s analyze this suggestion from the domain-driven design perspective.
评论作者说这是别人已经解决的问题,而且解决方案一应俱全。不用说,扫描技术和AR应用程序都很复杂。在 DDD 术语中,这使得检查冰箱是否适合通过门口的问题成为通用子域。
The author of the comment says this is a problem that others have already solved, and the solution is readily available. Needless to say, both the scanning technology and the AR application are complex. In DDD lingo, that makes the problem of checking whether the refrigerator will fit through the doorway a generic subdomain.
每当我们偶然发现领域专家心智模型中的内在冲突时,我们都必须将无处不在的语言分解为多个有界上下文。无处不在的语言应该在其限界上下文范围内保持一致。但是,在有界上下文中,相同的术语可能具有不同的含义。
Whenever we stumble upon an inherent conflict in the domain experts’ mental models, we have to decompose the ubiquitous language into multiple bounded contexts. A ubiquitous language should be consistent within the scope of its bounded context. However, across bounded contexts, the same terms can have different meanings.
在发现子域的同时,设计了有界上下文。将域划分为限界上下文是一项战略设计决策。
While subdomains are discovered, bounded contexts are designed. The division of the domain into bounded contexts is a strategic design decision.
有界上下文及其通用语言可以由一个团队实施和维护。没有两个团队可以在同一个限界上下文中共享工作。但是,一个团队可以处理多个限界上下文。
A bounded context and its ubiquitous language can be implemented and maintained by one team. No two teams can share the work on the same bounded context. However, one team can work on multiple bounded contexts.
限界上下文将系统分解为物理组件——服务、子系统等。每个有界上下文的生命周期都与其余部分分离。每个有界上下文都可以独立于系统的其余部分而发展。但是,限界上下文必须一起工作才能形成一个系统。一些更改会无意中影响另一个有界上下文。在下一章中,我们将讨论用于集成限界上下文的不同模式,这些模式可用于保护它们免受级联更改的影响。
Bounded contexts decompose a system into physical components—services, subsystems, and so on. Each bounded context’s lifecycle is decoupled from the rest. Each bounded context can evolve independently from the rest of the system. However, the bounded contexts have to work together to form a system. Some of the changes will inadvertently affect another bounded context. In the next chapter, we’ll talk about the different patterns for integrating bounded contexts that can be used to protect them from cascading changes.
子域和限界上下文有什么区别?
设计子域,同时发现有界上下文。
设计有界上下文,同时发现子域。
有界上下文和子域本质上是相同的。
以上都不是真的。
What is the difference between subdomains and bounded contexts?
Subdomains are designed, while bounded contexts are discovered.
Bounded contexts are designed, while subdomains are discovered.
Bounded contexts and subdomains are essentially the same.
None of the above is true.
有界上下文是以下内容的边界:
一个模型
一个生命周期
所有权
上述所有的
A bounded context is a boundary of:
A model
A lifecycle
Ownership
All of the above
关于限界上下文的大小,以下哪项是正确的?
限界上下文越小,系统越灵活。
限界上下文应始终与子域的边界对齐。
有界上下文越宽越好。
这取决于。
Which of the following is true regarding the size of a bounded context?
The smaller the bounded context is, the more flexible the system is.
Bounded contexts should always be aligned with the boundaries of subdomains.
The wider the bounded context is, the better.
It depends.
关于限界上下文的团队所有权,以下哪项是正确的?
多个团队可以在同一个限界上下文中工作。
一个团队可以拥有多个限界上下文。
限界上下文只能由一个团队拥有。
B 和 C 是正确的。
Which of the following is true regarding team ownership of a bounded context?
Multiple teams can work on the same bounded context.
A single team can own multiple bounded contexts.
A bounded context can be owned by one team only.
B and C are correct.
查看前言中 WolfDesk 公司的示例,并尝试识别可能需要不同模型的支持票证的系统功能。
Review the example of the WolfDesk company in the Preface and try to identify functionalities of the system that may require different models of a support ticket.
除了本章中描述的那些之外,尝试找到现实生活中有界上下文的例子。
Try to find examples of real-life bounded contexts, in addition to those described in this chapter.
1个这里有一个例外值得一提。根据您所在的组织,您可能身兼两职,同时负责软件工程和业务开发。因此,您有能力影响软件设计(限界上下文)和业务策略(子域)。因此,在我们这里讨论的(有界)上下文中,我们只关注软件工程。
1 There is an exception here that is worth mentioning. Depending on the organization you are working in, you may be wearing two hats and be in charge of both software engineering and business development. As a result, you have the ability to affect both the software design (bounded contexts) and the business strategy (subdomains). Therefore, in the (bounded) context of our discussion here, we are focusing only on software engineering.
2个Bredemeyer Consulting,“什么是软件架构”。2021 年 9 月 22 日检索,https://www.bredemeyer.com/who.htm
2 Bredemeyer Consulting, “What Is Software Architecture.” Retrieved September 22, 2021, https://www.bredemeyer.com/who.htm
限界上下文模式不仅保护了通用语言的一致性,还支持建模。您不能在不指定其目的(边界)的情况下构建模型。边界划分了语言的责任。一种有界上下文中的语言可以对业务领域进行建模以解决特定问题。另一个限界上下文可以表示相同的业务实体,但对它们建模以解决不同的问题。
Not only does the bounded context pattern protect the consistency of a ubiquitous language, it also enables modeling. You cannot build a model without specifying its purpose—its boundary. The boundary divides the responsibility of languages. A language in one bounded context can model the business domain to solve a particular problem. Another bounded context can represent the same business entities but model them to solve a different problem.
此外,不同限界上下文中的模型可以独立发展和实现。也就是说,有界上下文本身并不是独立的。正如系统不能由独立的组件构建——组件必须相互交互以实现系统的总体目标——在有界上下文中实现也是如此。虽然它们可以独立发展,但它们必须相互融合。因此,限界上下文之间总会有接触点。这些称为合同。
Moreover, models in different bounded contexts can be evolved and implemented independently. That said, bounded contexts themselves are not independent. Just as a system cannot be built out of independent components—the components have to interact with one another to achieve the system’s overarching goals—so, too, do the implementations in bounded contexts. Although they can evolve independently, they have to integrate with one another. As a result, there will always be touchpoints between bounded contexts. These are called contracts.
对契约的需求源于限界上下文模型和语言的差异。由于每份合同都影响不止一方,因此需要对其进行定义和协调。此外,根据定义,两个限界上下文使用不同的通用语言。将使用哪种语言进行集成?这些集成问题应该由解决方案的设计来评估和解决。
The need for contracts results from differences in bounded contexts’ models and languages. Since each contract affects more than one party, they need to be defined and coordinated. Also, by definition, two bounded contexts are using different ubiquitous languages. Which language will be used for integration purposes? These integration concerns should be evaluated and addressed by the solution’s design.
在本章中,您将了解用于定义限界上下文之间的关系和集成的领域驱动设计模式。这些模式是由在有界上下文中工作的团队之间的协作性质驱动的。我们将这些模式分为三组,每组代表一种团队协作:合作、客户-供应商和独立方式。
In this chapter, you will learn about domain-driven design patterns for defining relationships and integrations between bounded contexts. These patterns are driven by the nature of collaboration between teams working on bounded contexts. We will divide the patterns into three groups, each representing a type of team collaboration: cooperation, customer–supplier, and separate ways.
Cooperation patterns relate to bounded contexts implemented by teams with well-established communication.
在最简单的情况下,这些是由单个团队实现的限界上下文。这也适用于具有相关目标的团队,其中一个团队的成功取决于另一个团队的成功,反之亦然。同样,这里的主要标准是团队沟通和协作的质量。
In the simplest case, these are bounded contexts implemented by a single team. This also applies to teams with dependent goals, where one team’s success depends on the success of the other, and vice versa. Again, the main criterion here is the quality of the teams’ communication and collaboration.
让我们看一下适合协作团队的两种 DDD 模式:伙伴关系模式和共享内核模式。
Let’s look at two DDD patterns suitable for cooperating teams: the partnership and shared kernel patterns.
在伙伴关系模型中,有界之间的集成上下文以临时方式进行协调。一个团队可以将 API 的变化通知第二个团队,第二个团队将合作并适应——没有戏剧性或冲突(见图4-1)。
In the partnership model, the integration between bounded contexts is coordinated in an ad hoc manner. One team can notify a second team about a change in the API, and the second team will cooperate and adapt—no drama or conflicts (see Figure 4-1).
这里整合的协调是双向的。没有一个团队规定用于定义合同的语言。团队可以找出差异并选择最合适的解决方案。此外,双方合作解决可能出现的任何整合问题。两支球队都没有兴趣阻止另一支球队。
The coordination of integration here is two-way. No one team dictates the language that is used for defining the contracts. The teams can work out the differences and choose the most appropriate solution. Also, both sides cooperate in solving any integration issues that might come up. Neither team is interested in blocking the other one.
以这种方式成功集成需要良好的协作实践、高度的承诺和团队之间的频繁同步。从技术角度来看,需要持续集成两个团队应用的更改,以进一步最小化集成反馈循环。
Well-established collaboration practices, high levels of commitment, and frequent synchronizations between teams are required for successful integration in this manner. From a technical perspective, continuous integration of the changes applied by both teams is needed to further minimize the integration feedback loop.
这种模式可能不太适合地理分布的团队,因为它可能会带来同步和通信方面的挑战。
This pattern might not be a good fit for geographically distributed teams since it may present synchronization and communication challenges.
我们将检查的第二组协作模式是客户-供应商模式。如图4-3所示,限界上下文之一——供应商——为其客户提供服务。服务提供商是“上游”,客户或消费者是“下游”。
The second group of collaboration patterns we’ll examine is the customer–supplier patterns. As shown in Figure 4-3, one of the bounded contexts—the supplier—provides a service for its customers. The service provider is “upstream” and the customer or consumer is “downstream.”
与合作案例不同,两个团队(上游和下游)都可以独立成功。因此,在大多数情况下,我们的权力不平衡:上游或下游团队都可以决定集成合同。
Unlike in the cooperation case, both teams (upstream and downstream) can succeed independently. Consequently, in most cases we have an imbalance of power: either the upstream or the downstream team can dictate the integration contract.
本节将讨论解决此类权力差异的三种模式:遵从者模式、反腐败层模式和开放主机服务模式。
This section will discuss three patterns addressing such power differences: the conformist, anticorruption layer, and open-host service patterns.
在某些情况下,权力平衡有利于上游团队,它没有真正的动机来支持其客户的需求。相反,它只是提供根据自己的模型定义的集成合同——接受或离开。这种权力失衡可能是由于与组织外部的服务提供商的整合造成的,也可能仅仅是由于组织政治造成的。
In some cases, the balance of power favors the upstream team, which has no real motivation to support its clients’ needs. Instead, it just provides the integration contract, defined according to its own model—take it or leave it. Such power imbalances can be caused by integration with service providers that are external to the organization or simply by organizational politics.
如果下游团队可以接受上游团队的模型,则限界上下文关系称为conformist。下游遵循上游限界上下文的模型,如图4-4所示。
If the downstream team can accept the upstream team’s model, the bounded contexts’ relationship is called conformist. The downstream conforms to the upstream bounded context’s model, as shown in Figure 4-4.
下游团队放弃部分自主权的决定有多种理由。例如,上游团队暴露的合约可能是行业标准的、成熟的模型,也可能刚好满足下游团队的需求。
The downstream team’s decision to give up some of its autonomy can be justified in multiple ways. For example, the contract exposed by the upstream team may be an industry-standard, well-established model, or it may just be good enough for the downstream team’s needs.
下一个模式解决了消费者不愿意接受供应商模型的情况。
The next pattern addresses the case in which a consumer is not willing to accept the supplier’s model.
就像循规蹈矩的模式一样,这种关系中的权力平衡仍然偏向于上游服务。然而,在这种情况下,下游限界上下文并不愿意遵守。相反,它可以通过反腐败层将上游限界上下文的模型转换为适合自己需求的模型,如图4-5所示。
As in the conformist pattern, the balance of power in this relationship is still skewed toward the upstream service. However, in this case, the downstream bounded context is not willing to conform. Instead, it can translate the upstream bounded context’s model into a model tailored to its own needs via an anticorruption layer, as shown in Figure 4-5.
反腐败层模式解决了不希望或不值得努力遵循供应商模型的场景,例如:
The anticorruption layer pattern addresses scenarios in which it is not desirable or worth the effort to conform to the supplier’s model, such as the following:
从建模的角度来看,供应商模型的翻译将下游消费者与与其限界上下文无关的外来概念隔离开来。因此,它简化了消费者无处不在的语言和模型。
From a modeling perspective, the translation of the supplier’s model isolates the downstream consumer from foreign concepts that are not relevant to its bounded context. Hence, it simplifies the consumer’s ubiquitous language and model.
在第 9 章中,我们将探讨实现反腐败层的不同方法。
In Chapter 9, we will explore the different ways to implement an anticorruption layer.
这种模式解决了权力偏向于消费者。供应商有兴趣保护其消费者并提供尽可能最好的服务。
This pattern addresses cases in which the power is skewed toward the consumers. The supplier is interested in protecting its consumers and providing the best service possible.
为了保护消费者免受其实现模型更改的影响,上游供应商将实现模型与公共接口分离。这种解耦允许供应商以不同的速度发展其实现和公共模型,如图4-6所示。
To protect the consumers from changes in its implementation model, the upstream supplier decouples the implementation model from the public interface. This decoupling allows the supplier to evolve its implementation and public models at different rates, as shown in Figure 4-6.
供应商的公共接口无意符合其通用语言。相反,它旨在公开一种方便消费者的协议,以面向集成的语言表达。因此,公共协议被称为发布语言。
The supplier’s public interface is not intended to conform to its ubiquitous language. Instead, it is intended to expose a protocol convenient for the consumers, expressed in an integration-oriented language. As such, the public protocol is called the published language.
从某种意义上说,开放主机服务模式是反腐败层模式的逆转:供应商代替消费者实施其内部模型的翻译。
In a sense, the open-host service pattern is a reversal of the anticorruption layer pattern: instead of the consumer, the supplier implements the translation of its internal model.
将限界上下文的实现和集成模型解耦,使上游限界上下文可以自由地发展其实现,而不会影响下游上下文。当然,这只有在修改后的实施模型可以翻译成消费者已经在使用的已发布语言的情况下才有可能。
Decoupling the bounded context’s implementation and integration models gives the upstream bounded context the freedom to evolve its implementation without affecting the downstream contexts. Of course, that’s only possible if the modified implementation model can be translated into the published language the consumers are already using.
此外,集成模型的解耦允许上游限界上下文同时暴露已发布语言的多个版本,从而允许消费者逐步迁移到新版本(见图 4-7 )。
Furthermore, the integration model’s decoupling allows the upstream bounded context to simultaneously expose multiple versions of the published language, allowing the consumer to migrate to the new version gradually (see Figure 4-7).
最后一个合作选项是根本不合作。在团队不愿意或无法协作的情况下,这种模式可能出于不同的原因而出现。我们将在这里查看其中的一些。
The last collaboration option is not to collaborate at all. This pattern can arise for different reasons, in cases where the teams are unwilling or unable to collaborate. We’ll look at a few of them here.
避免协作的一个常见原因是沟通由组织规模或内部政治驱动的困难。当团队难以协作和达成一致时,分道扬镳并在多个有界上下文中复制功能可能更具成本效益。
A common reason for avoiding collaboration is communication difficulties driven by the organization’s size or internal politics. When teams have a hard time collaborating and agreeing, it may be more cost-effective to go their separate ways and duplicate functionality in multiple bounded contexts.
重复子域的性质也可能是团队的一个原因各奔东西。当所讨论的子域是通用的,并且通用解决方案易于集成时,将其本地集成到每个限界上下文中可能更具成本效益。一个例子是日志框架;将其中一个限界上下文作为服务公开是没有意义的。集成此类解决方案所增加的复杂性将超过不在多个上下文中复制功能的好处。复制功能比协作成本更低。
The nature of the duplicated subdomain can also be a reason for teams to go their separate ways. When the subdomain in question is generic, and if the generic solution is easy to integrate, it may be more cost-effective to integrate it locally in each bounded context. An example is a logging framework; it would make little sense for one of the bounded contexts to expose it as a service. The added complexity of integrating such a solution would outweigh the benefit of not duplicating the functionality in multiple contexts. Duplicating the functionality would be less expensive than collaborating.
限界上下文模型的差异也可能是一个原因以分道扬镳的方式进行协作。这些模型可能非常不同,以至于不可能建立一致的关系,并且实施反腐败层比复制功能更昂贵。在这种情况下,各团队分道扬镳也更具成本效益。
Differences in the bounded contexts’ models can also be a reason to go with a separate ways collaboration. The models may be so different that a conformist relationship is impossible, and implementing an anticorruption layer would be more expensive than duplicating the functionality. In such a case, it is again more cost-effective for the teams to go their separate ways.
集成核心子域时应避免分离方式模式。重复实施此类子域将违背公司以最有效和优化的方式实施它们的战略。
The separate ways pattern should be avoided when integrating core subdomains. Duplicating the implementation of such subdomains would defy the company’s strategy to implement them in the most effective and optimized way.
在分析了系统限界上下文之间的集成模式之后,我们可以将它们绘制在上下文图上,如图4-8所示。
After analyzing the integration patterns between a system’s bounded contexts, we can plot them on a context map, as shown in Figure 4-8.
上下文映射是系统的限界上下文和它们之间的集成的可视化表示。这种视觉符号在多个层面上提供了宝贵的战略洞察力:
The context map is a visual representation of the system’s bounded contexts and the integrations between them. This visual notation gives valuable strategic insight on multiple levels:
理想情况下,应将上下文映射引入项目中从一开始就更新,并进行更新以反映新的有界上下文的添加和对现有上下文的修改。
Ideally, a context map should be introduced into a project right from the get-go, and be updated to reflect additions of new bounded contexts and modifications to the existing one.
由于上下文映射可能包含来自多个团队工作的信息,因此最好将上下文映射的维护定义为共同努力:每个团队负责更新自己与其他限界上下文的集成。
Since the context map potentially contains information originating from the work of multiple teams, it’s best to define the maintenance of the context map as a shared effort: each team is responsible for updating its own integrations with other bounded contexts.
使用Context Mapper等工具,可以将上下文映射作为代码进行管理和维护。
A context map can be managed and maintained as code, using a tool like Context Mapper.
重要的是要注意绘制上下文地图可以是具有挑战性的任务。当系统的限界上下文包含多个子域时,可能会有多种集成模式在起作用。例如,在图 4-9中,您可以看到具有两种集成模式的两个限界上下文:伙伴关系和反腐败层。
It’s important to note that charting a context map can be a challenging task. When a system’s bounded contexts encompass multiple subdomains, there can be multiple integration patterns at play. For example, in Figure 4-9, you can see two bounded contexts with two integration patterns: partnership and anticorruption layer.
此外,即使限界上下文仅限于单个子域,仍然可以有多种集成模式在起作用——例如,如果子域的模块需要不同的集成策略。
Moreover, even if bounded contexts are limited to a single subdomain, there still can be multiple integration patterns at play—for example, if the subdomains’ modules require different integration strategies.
限界上下文不是独立的。他们必须彼此互动。以下模式定义了可以集成限界上下文的不同方式:
Bounded contexts are not independent. They have to interact with one another. The following patterns define different ways bounded contexts can be integrated:
有界上下文之间的集成可以绘制在上下文映射上。该工具可以深入了解系统的高级设计、通信模式和组织问题。
The integrations among the bounded contexts can be plotted on a context map. This tool gives insight into the system’s high-level design, communication patterns, and organizational issues.
现在您已经了解了用于分析和建模业务领域的领域驱动设计工具和技术,我们将把我们的视角从战略转向战术。在第 II 部分中,您将了解实现域逻辑、组织高级架构以及协调系统组件之间通信的不同方法。
Now that you have learned about the domain-driven design tools and techniques for analyzing and modeling business domains, we will shift our perspective from strategy to tactics. In Part II, you’ll learn different ways to implement domain logic, organize high-level architecture, and coordinate communication between a system’s components.
哪种集成模式不应该用于核心子域?
共享内核
开放主机服务
反腐层
分道扬镳
Which integration pattern should never be used for a core subdomain?
Shared kernel
Open-host service
Anticorruption layer
Separate ways
哪个下游子域更有可能实施反腐败层?
核心子域
支持子域名
通用子域
乙丙
Which downstream subdomain is more likely to implement an anticorruption layer?
Core subdomain
Supporting subdomain
Generic subdomain
B and C
哪个上游子域更有可能实现开放主机服务?
核心子域
支持子域名
通用子域
甲乙
Which upstream subdomain is more likely to implement an open-host service?
Core subdomain
Supporting subdomain
Generic subdomain
A and B
在某种意义上,哪种集成模式违反了限界上下文的所有权边界?
合伙。
共享内核。
分开的方式。
任何集成模式都不应打破限界上下文的所有权边界。
Which integration pattern, in a sense, violates bounded contexts’ ownership boundaries?
Partnership.
Shared kernel.
Separate ways.
No integration pattern should ever break the bounded contexts’ ownership boundaries.
在第一部分,我们讨论了软件的“什么”和“为什么”:你学会了分析业务领域,识别子领域及其战略价值,并将业务领域的知识转化为限界上下文的设计——实现不同模型的软件组件的业务领域。
In Part I, we discussed the “what” and “why” of software: you learned to analyze business domains, identify subdomains and their strategic value, and turn the knowledge of business domains into the design of bounded contexts—software components implementing different models of the business domain.
在本书的这一部分,我们将从战略转向战术:软件设计的“方式”:
In this part of the book, we will turn from strategy to tactics: the “how” of software design:
在第 5章到第 7章中,您将学习业务逻辑实现模式,这些模式允许代码使用其限界上下文的通用语言。第 5 章介绍了两种适用于相对简单业务逻辑的模式:事务脚本和活动记录。第 6 章转向更具挑战性的案例并介绍领域模型模式:DDD 实现复杂业务逻辑的方式。在第 7 章中,您将学习通过对时间维度进行建模来扩展领域模型模式。
In Chapters 5 through 7, you will learn business logic implementation patterns that allow the code to speak the ubiquitous language of its bounded context. Chapter 5 introduces two patterns that accommodate a relatively simple business logic: transaction script and active record. Chapter 6 moves to more challenging cases and presents the domain model pattern: DDD’s way of implementing complex business logic. In Chapter 7, you will learn to expand the domain model pattern by modeling the dimension of time.
在第 8 章中,我们将探讨组织限界上下文架构的不同方式:分层架构、端口和适配器以及 CQRS 模式。您将了解每种架构模式的本质,以及每种模式应该在哪些情况下使用。
In Chapter 8, we will explore the different ways to organize a bounded context’s architecture: the layered architecture, ports & adapters, and CQRS patterns. You will learn the essence of each architectural pattern and in which cases each pattern should be used.
第 9 章将讨论协调系统组件间交互的技术问题和实施策略。您将学习支持限界上下文集成模式实施的模式、如何实施可靠的消息发布以及用于定义复杂的跨组件工作流的模式。
Chapter 9 will discuss technical concerns and implementation strategies for orchestrating the interactions among components of a system. You will learn patterns supporting the implementation of bounded context integration patterns, how to implement reliable publishing of messages, and patterns for defining complex, cross-component workflows.
业务逻辑是软件最重要的部分。这就是首先实施该软件的原因。一个系统的用户界面可以很吸引人,它的数据库可以非常快速和可扩展。但是,如果该软件对业务没有用,那么它只不过是一个昂贵的技术演示。
Business logic is the most important part of software. It’s the reason the software is being implemented in the first place. A system’s user interface can be sexy and its database can be blazing fast and scalable. But if the software is not useful for the business, it’s nothing but an expensive technology demo.
正如我们在第 2 章中看到的,并非所有的业务子域都是生而平等的。不同的子域具有不同级别的战略重要性和复杂性。本章开始探索建模和实现业务逻辑代码的不同方法。我们将从适合于相当简单的业务逻辑的两种模式开始:事务脚本和活动记录。
As we saw in Chapter 2, not all business subdomains are created equal. Different subdomains have different levels of strategic importance and complexity. This chapter begins our exploration of the different ways to model and implement business logic code. We will start with two patterns suited for rather simple business logic: transaction script and active record.
通过过程组织业务逻辑,其中每个过程处理来自演示文稿的单个请求。
马丁·福勒一
Organizes business logic by procedures where each procedure handles a single request from the presentation.
Martin Fowler1
系统的公共接口可以看作是消费者可以执行的业务事务的集合,如图5-1所示。这些事务可以检索系统管理的信息,修改它,或两者兼而有之。该模式根据过程组织系统的业务逻辑,其中每个过程实现一个操作,该操作由系统的消费者通过其公共接口执行。实际上,系统的公共操作被用作封装边界。
A system’s public interface can be seen as a collection of business transactions that consumers can execute, as shown in Figure 5-1. These transactions can retrieve information managed by the system, modify it, or both. The pattern organizes the system’s business logic based on procedures, where each procedure implements an operation that is executed by the system’s consumer via its public interface. In effect, the system’s public operations are used as encapsulation boundaries.
每个过程都作为一个简单、直接的过程脚本来实现。它可以使用一个薄的抽象层来集成存储机制,但它也可以直接访问数据库。
Each procedure is implemented as a simple, straightforward procedural script. It can use a thin abstraction layer for integrating with storage mechanisms, but it is also free to access the databases directly.
程序必须满足的唯一要求是交易行为。每个操作都应该成功或失败,但永远不会导致无效状态。即使交易脚本的执行在最不方便的时刻失败,系统也应该保持一致——要么通过回滚它所做的任何更改直到失败,要么通过执行补偿操作。事务行为反映在模式的名称中:事务脚本。
The only requirement procedures have to fulfill is transactional behavior. Each operation should either succeed or fail but can never result in an invalid state. Even if execution of a transaction script fails at the most inconvenient moment, the system should remain consistent—either by rolling back any changes it has made up until the failure or by executing compensating actions. The transactional behavior is reflected in the pattern’s name: transaction script.
以下是将成批的 JSON 文件转换为 XML 文件的事务脚本示例:
Here is an example of a transaction script that converts batches of JSON files into XML files:
DB.StartTransaction();varjob=DB.LoadNextJob();varjson=LoadFile(job.Source);varxml=ConvertJsonToXml(json);WriteFile(job.Destination,xml.ToString();DB.MarkJobAsCompleted(job);DB.Commit()
DB.StartTransaction();varjob=DB.LoadNextJob();varjson=LoadFile(job.Source);varxml=ConvertJsonToXml(json);WriteFile(job.Destination,xml.ToString();DB.MarkJobAsCompleted(job);DB.Commit()
当我在我的领域驱动设计课程中介绍事务脚本模式时,我的学生经常会竖起眉毛,有些人甚至会问,“这值得我们花时间吗?我们来这里不是为了更高级的模式和技术吗?”
When I introduce the transaction script pattern in my domain-driven design classes, my students often raise their eyebrows, and some even ask, “Is it worth our time? Aren’t we here for the more advanced patterns and techniques?”
事实上,事务脚本模式是您将在接下来的章节中学习的更高级业务逻辑实现模式的基础。此外,尽管它看起来很简单,但它是最容易出错的模式。我以某种方式帮助调试和修复的大量生产问题,通常归结为系统业务逻辑的事务行为的错误实现。
The thing is, the transaction script pattern is a foundation for the more advanced business logic implementation patterns you will learn in the forthcoming chapters. Furthermore, despite its apparent simplicity, it is the easiest pattern to get wrong. A considerable number of production issues I have helped to debug and fix, in one way or another, often boiled down to a misimplementation of the transactional behavior of the system’s business logic.
让我们看一下由于未能正确实施事务脚本而导致的三个常见的真实数据损坏示例。
Let’s take a look at three common, real-life examples of data corruption that results from failing to correctly implement a transaction script.
未能实现事务行为的一个简单示例是在没有总体事务的情况下发布多个更新。考虑以下更新 Users 表中的记录并将记录插入到 VisitsLog 表中的方法:
A trivial example of failing to implement transactional behavior is to issue multiple updates without an overarching transaction. Consider the following method that updates a record in the Users table and inserts a record into the VisitsLog table:
01publicclassLogVisit02{03...0405publicvoidExecute(GuiduserId,DataTimevisitedOn)06{07_db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2",08visitedOn,userId);09_db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date)10 VALUES(@p1, @p2)",userId,visitedOn);11}12}
01publicclassLogVisit02{03...0405publicvoidExecute(GuiduserId,DataTimevisitedOn)06{07_db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2",08visitedOn,userId);09_db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date)10 VALUES(@p1, @p2)",userId,visitedOn);11}12}
如果在更新 Users 表中的记录(第 7 行)但在第 9 行附加日志记录成功之前出现任何问题,系统将最终处于不一致状态。Users 表将被更新,但不会将相应的记录写入 VisitsLog 表。问题可能是由于网络中断、数据库超时或死锁,甚至是执行该进程的服务器崩溃引起的。
If any issue occurs after the record in the Users table was updated (line 7) but before appending the log record on line 9 succeeds, the system will end up in an inconsistent state. The Users table will be updated but no corresponding record will be written to the VisitsLog table. The issue can be due to anything from a network outage to a database timeout or deadlock, or even a crash of the server executing the process.
这可以通过引入包含两个数据更改的适当事务来解决:
This can be fixed by introducing a proper transaction encompassing both data changes:
publicclassLogVisit{...publicvoidExecute(GuiduserId,DataTimevisitedOn){try{_db.StartTransaction();_db.Execute(@"UPDATE Users SET last_visit=@p1WHERE user_id=@p2",visitedOn,userId);_db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date)VALUES(@p1, @p2)",userId,visitedOn);_db.Commit();}catch{_db.Rollback();throw;}}}
publicclassLogVisit{...publicvoidExecute(GuiduserId,DataTimevisitedOn){try{_db.StartTransaction();_db.Execute(@"UPDATE Users SET last_visit=@p1WHERE user_id=@p2",visitedOn,userId);_db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date)VALUES(@p1, @p2)",userId,visitedOn);_db.Commit();}catch{_db.Rollback();throw;}}}
由于关系数据库对跨多个记录的事务的本机支持,因此修复很容易实施。当您必须在不支持多记录事务的数据库中发布多个更新时,或者当您使用不可能在分布式事务中统一的多个存储机制时,事情会变得更加复杂。让我们看一个后一种情况的例子。
The fix is easy to implement due to relational databases’ native support of transactions spanning multiple records. Things get more complicated when you have to issue multiple updates in a database that doesn’t support multirecord transactions, or when you are working with multiple storage mechanisms that are impossible to unite in a distributed transaction. Let’s see an example of the latter case.
在现代分布式系统中,通常的做法是更改数据库中的数据,然后通过将消息发布到消息总线来将更改通知系统的其他组件。考虑在前面的示例中,我们必须将访问发布到消息总线,而不是在表中记录访问:
In modern distributed systems, it’s a common practice to make changes to the data in a database and then notify other components of the system about the changes by publishing messages into a message bus. Consider that in the previous example, instead of logging a visit in a table, we have to publish it to a message bus:
01publicclassLogVisit02{03...0405publicvoidExecute(GuiduserId,DataTimevisitedOn)06{07_db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2",08visitedOn,userId);09_messageBus.Publish("VISITS_TOPIC",10new{UserId=userId,VisitDate=visitedOn});11}12}
01publicclassLogVisit02{03...0405publicvoidExecute(GuiduserId,DataTimevisitedOn)06{07_db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2",08visitedOn,userId);09_messageBus.Publish("VISITS_TOPIC",10new{UserId=userId,VisitDate=visitedOn});11}12}
与前面的示例一样,在第 7 行之后但在第 9 行成功之前发生的任何故障都将破坏系统的状态。Users 表将更新,但不会通知其他组件,因为发布到消息总线失败。
As in the previous example, any failure occurring after line 7 but before line 9 succeeds will corrupt the system’s state. The Users table will be updated but the other components won’t be notified as publishing to the message bus has failed.
不幸的是,解决这个问题并不像前面的例子那么容易。跨越多个存储机制的分布式事务复杂、难以扩展、容易出错,因此通常被避免。在第 8 章中,您将学习如何使用 CQRS 架构模式来填充多种存储机制。此外,第 9 章将介绍发件箱模式,它可以在将更改提交到另一个数据库后可靠地发布消息。
Unfortunately, fixing the issue is not as easy as in the previous example. Distributed transactions spanning multiple storage mechanisms are complex, hard to scale, error prone, and therefore are usually avoided. In Chapter 8, you will learn how to use the CQRS architectural pattern to populate multiple storage mechanisms. In addition, Chapter 9 will introduce the outbox pattern, which enables reliable publishing of messages after committing changes to another database.
让我们看一个更复杂的交易行为实施不当的例子。
Let’s see a more intricate example of improper implementation of transactional behavior.
考虑以下看似简单的方法:
Consider the following deceptively simple method:
publicclassLogVisit{...publicvoidExecute(GuiduserId){_db.Execute("UPDATE Users SET visits=visits+1 WHERE user_id=@p1",userId);}}
publicclassLogVisit{...publicvoidExecute(GuiduserId){_db.Execute("UPDATE Users SET visits=visits+1 WHERE user_id=@p1",userId);}}
此方法不是像前面的示例那样跟踪上次访问日期,而是为每个用户维护一个访问计数器。调用该方法会将相应计数器的值增加 1。该方法所做的只是更新驻留在一个数据库中的一个表中的一个值。然而,这仍然是一个可能导致状态不一致的分布式事务。
Instead of tracking the last visit date as in the previous examples, this method maintains a counter of visits for each user. Calling the method increases the corresponding counter’s value by 1. All the method does is update one value, in one table, residing in one database. Yet this is still a distributed transaction that can potentially lead to inconsistent state.
这个例子构成了一个分布式事务,因为它向数据库和调用该方法的外部进程传递信息,如图5-2所示。
This example constitutes a distributed transaction because it communicates information to the databases and the external process that called the method, as demonstrated in Figure 5-2.
LogVisit并通知调用者操作成功或失败的操作尽管该execute方法是 类型的void,即它不返回任何数据,但它仍然传达操作是成功还是失败:如果失败,调用者将得到一个异常。如果方法成功,但将结果传递给调用者失败怎么办?例如:
Although the execute method is of type void, that is, it doesn’t return any data, it still communicates whether the operation has succeeded or failed: if it failed, the caller will get an exception. What if the method succeeds, but the communication of the result to the caller fails? For example:
如果LogVisit是 REST 服务的一部分并且出现网络中断;或者
If LogVisit is part of a REST service and there is a network outage; or
如果两者LogVisit和调用者都在同一个进程中运行,但在调用者跟踪操作的成功执行之前进程失败了LogVisit?
If both LogVisit and the caller are running in the same process, but the process fails before the caller gets to track successful execution of the LogVisit action?
在这两种情况下,消费者都会假设失败并LogVisit再次尝试调用。再次执行该LogVisit逻辑将导致计数器值的错误增加。总的来说,它会增加 2 而不是 1。如前两个示例中,代码未能正确实现事务脚本模式,并无意中导致破坏系统状态。
In both cases, the consumer will assume failure and try calling LogVisit again. Executing the LogVisit logic again will result in an incorrect increase of the counter’s value. Overall, it will be increased by 2 instead of 1. As in the previous two examples, the code fails to implement the transaction script pattern correctly, and inadvertently leads to corrupting the system’s state.
与前面的示例一样,此问题没有简单的修复方法。这完全取决于业务领域及其需求。在此特定示例中,确保事务行为的一种方法是使操作幂等:即,即使操作重复多次,也会导致相同的结果。
As in the previous example, there is no simple fix for this issue. It all depends on the business domain and its needs. In this specific example, one way to ensure transactional behavior is to make the operation idempotent: that is, leading to the same result even if the operation repeated multiple times.
例如,我们可以要求消费者传递计数器的值。要提供计数器的值,调用者必须首先读取当前值,在本地增加它,然后将更新后的值作为参数提供。即使该操作将被执行多次,也不会改变最终结果:
For example, we can ask the consumer to pass the value of the counter. To supply the counter’s value, the caller will have to read the current value first, increase it locally, and then provide the updated value as a parameter. Even if the operation will be executed multiple times, it won’t change the end result:
publicclassLogVisit{...publicvoidExecute(GuiduserId,longvisits){_db.Execute("UPDATE Users SET visits = @p1 WHERE user_id=@p2",visits,userId);}}
publicclassLogVisit{...publicvoidExecute(GuiduserId,longvisits){_db.Execute("UPDATE Users SET visits = @p1 WHERE user_id=@p2",visits,userId);}}
解决此类问题的另一种方法是使用乐观并发控制:在调用操作之前LogVisit,调用者已读取计数器的当前值并将其LogVisit作为参数传递给。LogVisit仅当计数器的值等于调用者最初读取的值时才会更新计数器的值:
Another way to address such an issue is to use optimistic concurrency control: prior to calling the LogVisit operation, the caller has read the counter’s current value and passed it to LogVisit as a parameter. LogVisit will update the counter’s value only if it equals the one initially read by the caller:
publicclassLogVisit{...publicvoidExecute(GuiduserId,longexpectedVisits){_db.Execute(@"UPDATE Users SET visits=visits+1WHERE user_id=@p1 and visits = @p2",userId,visits);}}
publicclassLogVisit{...publicvoidExecute(GuiduserId,longexpectedVisits){_db.Execute(@"UPDATE Users SET visits=visits+1WHERE user_id=@p1 and visits = @p2",userId,visits);}}
使用相同输入参数的后续执行LogVisit不会更改数据,因为WHERE...visits = @prm2不会满足条件。
Subsequent executions of LogVisit with the same input parameters won’t change the data, as the WHERE...visits = @prm2 condition won’t be fulfilled.
事务脚本模式很好地适应了最直接的业务逻辑类似于简单过程操作的问题域。例如,在提取-转换-加载 (ETL) 操作中,每个操作都从源中提取数据,应用转换逻辑将其转换为另一种形式,并将结果加载到目标存储中。这个过程如图 5-3所示。
The transaction script pattern is well adapted to the most straightforward problem domains in which the business logic resembles simple procedural operations. For example, in extract-transform-load (ETL) operations, each operation extracts data from a source, applies transformation logic to convert it into another form, and loads the result into the destination store. This process is shown in Figure 5-3.
事务脚本模式自然适合支持子域,根据定义,业务逻辑很简单。它还可以用作与外部系统集成的适配器——例如,通用子域,或作为反腐败层的一部分(第 9 章对此有更多介绍)。
The transaction script pattern naturally fits supporting subdomains where, by definition, the business logic is simple. It can also be used as an adapter for integration with external systems—for example, generic subdomains, or as a part of an anticorruption layer (more on that in Chapter 9).
事务脚本模式的主要优点是简单。它引入了最少的抽象,并最大限度地减少了运行时性能和理解业务逻辑方面的开销。也就是说,这种简单性也是该模式的缺点。业务逻辑越复杂,就越容易跨事务复制业务逻辑,因此,当复制的代码不同步时,就会导致不一致的行为。因此,事务脚本不应该用于核心子域,因为这种模式无法应对核心子域业务逻辑的高度复杂性。
The main advantage of the transaction script pattern is its simplicity. It introduces minimal abstractions and minimizes the overhead both in runtime performance and in understanding the business logic. That said, this simplicity is also the pattern’s disadvantage. The more complex the business logic gets, the more it’s prone to duplicate business logic across transactions, and consequently, to result in inconsistent behavior—when the duplicated code goes out of sync. As a result, transaction script should never be used for core subdomains, as this pattern won’t cope with the high complexity of a core subdomain’s business logic.
这种简单性为交易脚本赢得了可疑的声誉。有时模式甚至被视为反模式。毕竟,如果将复杂的业务逻辑实现为事务脚本,迟早会变成一个无法维护的大泥球。然而,应该注意的是,尽管很简单,但事务脚本模式在软件开发中无处不在。我们将在本章和后续章节中讨论的所有业务逻辑实现模式都以某种方式基于事务脚本模式。
This simplicity earned the transaction script a dubious reputation. Sometimes the pattern is even treated as an antipattern. After all, if complex business logic is implemented as a transaction script, sooner rather than later it’s going to turn into an unmaintainable, big ball of mud. It should be noted, however, that despite the simplicity, the transaction script pattern is ubiquitous in software development. All the business logic implementation patterns that we will discuss in this and the following chapters, in one way or another, are based on the transaction script pattern.
一个对象,它在数据库表或视图中包装一行,封装数据库访问,并在该数据上添加域逻辑。
马丁·福勒2
An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.
Martin Fowler2
与事务脚本模式一样,活动记录支持业务逻辑简单的情况。然而,这里的业务逻辑可能对更复杂的数据结构进行操作。例如,我们可以拥有更复杂的对象树和层次结构,而不是平面记录,如图5-4所示。
Like the transaction script pattern, active record supports cases where the business logic is simple. Here, however, the business logic may operate on more complex data structures. For example, instead of flat records, we can have more complicated object trees and hierarchies, as shown in Figure 5-4.
通过简单的事务脚本对此类数据结构进行操作会导致大量重复代码。数据到内存中表示的映射将在各处重复。
Operating on such data structures via a simple transaction script would result in lots of repetitive code. The mapping of the data to an in-memory representation would be duplicated all over.
因此,此模式使用称为活动记录的专用对象,表示复杂的数据结构。除了数据结构之外,这些对象还实现了创建、读取、更新和删除记录的数据访问方法,即所谓的 CRUD 操作。因此,活动记录对象耦合到对象关系映射 (ORM) 或其他一些数据访问框架。该模式的名称源于每个数据结构都是“活动的”这一事实;也就是说,它实现了数据访问逻辑。
Consequently, this pattern uses dedicated objects, known as active records, to represent complicated data structures. Apart from the data structure, these objects also implement data access methods for creating, reading, updating, and deleting records—the so-called CRUD operations. As a result, the active record objects are coupled to an object-relational mapping (ORM) or some other data access framework. The pattern’s name is derived from the fact that each data structure is “active”; that is, it implements data access logic.
和前面的模式一样,系统的业务逻辑是有组织的在交易脚本中。两种模式的区别在于,在这种情况下,事务脚本不是直接访问数据库,而是操作活动记录对象。当它完成时,该操作必须作为原子事务完成或失败:
As in the previous pattern, the system’s business logic is organized in a transaction script. The difference between the two patterns is that in this case, instead of accessing the database directly, the transaction script manipulates active record objects. When it completes, the operation has to either complete or fail as an atomic transaction:
publicclassCreateUser{...publicvoidExecute(userDetails){try{_db.StartTransaction();varuser=newUser();user.Name=userDetails.Name;user.=userDetails.;user.Save();_db.Commit();}catch{_db.Rollback();throw;}}}
publicclassCreateUser{...publicvoidExecute(userDetails){try{_db.StartTransaction();varuser=newUser();user.Name=userDetails.Name;user.=userDetails.;user.Save();_db.Commit();}catch{_db.Rollback();throw;}}}
该模式的目标是封装将内存中对象映射到数据库模式的复杂性。除了负责持久化之外,活动记录对象还可以包含业务逻辑;例如,验证分配给字段的新值,甚至实现操作对象数据的业务相关过程。也就是说,活动记录对象的显着特征是数据结构和行为(业务逻辑)的分离。通常,活动记录的字段具有允许外部程序修改其状态的公共 getter 和 setter。
The pattern’s goal is to encapsulate the complexity of mapping the in-memory object to the database’s schema. In addition to being responsible for persistence, the active record objects can contain business logic; for example, validating new values assigned to the fields, or even implementing business-related procedures that manipulate an object’s data. That said, the distinctive feature of an active record object is the separation of data structures and behavior (business logic). Usually, an active record’s fields have public getters and setters that allow external procedures to modify its state.
因为活动记录本质上是一个交易脚本优化了对数据库的访问,这种模式只能支持比较简单的业务逻辑,比如CRUD操作,最多就是验证用户的输入。
Because an active record is essentially a transaction script that optimizes access to databases, this pattern can only support relatively simple business logic, such as CRUD operations, which, at most, validate the user’s input.
因此,与事务脚本模式的情况一样,活动记录模式适用于支持子域、通用子域的外部解决方案集成或模型转换任务。模式之间的区别在于活动记录解决了将复杂数据结构映射到数据库模式的复杂性。
Accordingly, as in the case of the transaction script pattern, the active record pattern lends itself to supporting subdomains, integration of external solutions for generic subdomains, or model transformation tasks. The difference between the patterns is that active record addresses the complexity of mapping complicated data structures to a database’s schema.
活动记录模式也称为贫血领域模型反模式;换句话说,一个设计不当的领域模型。我更喜欢克制anemic和antipattern这两个词的负面含义。这种模式是一种工具。与任何工具一样,它可以解决问题,但如果在错误的环境中应用,它可能带来的弊大于利。当业务逻辑简单时使用活动记录没有错。此外,在实现简单的业务逻辑时使用更精细的模式也会因引入意外的复杂性而造成危害。在下一章中,您将了解什么是领域模型以及它与活动记录模式的区别。
The active record pattern is also known as an anemic domain model antipattern; in other words, an improperly designed domain model. I prefer to restrain from the negative connotation of the words anemic and antipattern. This pattern is a tool. Like any tool, it can solve problems, but it can potentially introduce more harm than good when applied in the wrong context. There is nothing wrong with using active records when the business logic is simple. Furthermore, using a more elaborate pattern when implementing simple business logic will also result in harm by introducing accidental complexity. In the next chapter, you will learn what a domain model is and how it differs from an active record pattern.
需要强调的是,在此上下文中,Active Record指的是设计模式,而不是 Active Record 框架。模式名称是由 Martin Fowler在Patterns of Enterprise Application Architecture中创造的。该框架后来作为实现该模式的一种方式出现。在我们的上下文中,我们谈论的是设计模式及其背后的概念,而不是具体的实现。
It’s important to stress that in this context, active record refers to the design pattern, not the Active Record framework. The pattern name was coined in Patterns of Enterprise Application Architecture by Martin Fowler. The framework came later as one way to implement the pattern. In our context, we are talking about the design pattern and the concepts behind it, not a specific implementation.
虽然业务数据很重要,我们设计的代码并且构建应该保护其完整性,在某些情况下更需要实用的方法。
Although business data is important and the code we design and build should protect its integrity, there are cases in which a pragmatic approach is more desirable.
尤其是在大规模的情况下,有些情况下可以放宽数据一致性保证。检查破坏 100 万条记录中的一条记录的状态是否真的会对企业造成影响,以及它是否会对企业的绩效和盈利能力产生负面影响。例如,假设您正在构建一个每天从 IoT 设备摄取数十亿个事件的系统。如果 0.001% 的事件将被复制或丢失,这有什么大不了的吗?
Especially at high levels of scale, there are cases when data consistency guarantees can be relaxed. Check whether corrupting the state of one record out of 1 million is really a showstopper for the business and whether it can negatively affect the performance and profitability of the business. For example, let’s assume you are building a system that ingests billions of events per day from IoT devices. Is it a big deal if 0.001% of the events will be duplicated or lost?
一如既往,没有普遍的规律。这完全取决于您所从事的业务领域。尽可能“偷工减料”是可以的;只需确保您评估了风险和业务影响。
As always, there are no universal laws. It all depends on the business domain you are working in. It’s OK to “cut corners” where possible; just make sure you evaluate the risks and business implications.
在本章中,我们介绍了两种实现业务逻辑的模式:
In this chapter, we covered two patterns for implementing business logic:
本章讨论的两种模式都是针对业务逻辑相当简单的情况。在下一章中,我们将转向更复杂的业务逻辑,并讨论如何使用领域模型模式来解决复杂性问题。
The two patterns discussed in this chapter are oriented toward cases of rather simple business logic. In the next chapter, we will turn to more complex business logic and discuss how to tackle the complexity using the domain model pattern.
应该使用所讨论的哪种模式来实现核心子域的业务逻辑?
交易脚本。
活动记录。
这些模式都不能用于实现核心子域。
两者都可用于实现核心子域。
Which of the discussed patterns should be used for implementing a core subdomain’s business logic?
Transaction script.
Active record.
Neither of these patterns can be used to implement a core subdomain.
Both can be used to implement a core subdomain.
考虑以下代码:
publicvoidCreateTicket(TicketDatadata){varagent=FindLeastBusyAgent();agent.ActiveTickets=agent.ActiveTickets+1;agent.Save();varticket=newTicket();ticket.Id=Guid.New();ticket.Data=data;ticket.AssignedAgent=agent;ticket.Save();_alerts.Send(agent,"You have a new ticket!");}
假设没有高级事务机制,您可以在这里发现哪些潜在的数据一致性问题?
收到新工单后,指定代理人的活跃工单计数器可以增加 1 以上。
代理的活动工单计数器可以增加 1,但不会为该代理分配任何新工单。
代理可以得到一张新票,但不会收到通知。
以上问题都有可能。
Consider the following code:
publicvoidCreateTicket(TicketDatadata){varagent=FindLeastBusyAgent();agent.ActiveTickets=agent.ActiveTickets+1;agent.Save();varticket=newTicket();ticket.Id=Guid.New();ticket.Data=data;ticket.AssignedAgent=agent;ticket.Save();_alerts.Send(agent,"You have a new ticket!");}
Assuming there is no high-level transaction mechanism, what potential data consistency issues can you spot here?
On receiving a new ticket, the assigned agent’s counter of active tickets can be increased by more than 1.
An agent’s counter of active tickets can be increased by 1 but the agent won’t get assigned any new tickets.
An agent can get a new ticket but won’t be notified about it.
All of the above issues are possible.
在前面的代码中,至少还有一种可能会破坏系统状态的边缘情况。你能发现吗?
In the preceding code, there is at least one more possible edge case that can corrupt the system’s state. Can you spot it?
回到本书前言中的 WolfDesk 示例,系统的哪些部分可能被实现为事务脚本或活动记录?
Going back to the example of WolfDesk in the book’s Preface, what parts of the system could potentially be implemented as a transaction script or an active record?
前一章讨论了两种解决相对简单业务逻辑的模式:事务脚本和活动记录。本章继续实现业务逻辑的话题,介绍了一种面向复杂业务逻辑的模式:领域模型模式。
The previous chapter discussed two patterns addressing cases of relatively simple business logic: transaction script and active record. This chapter continues the topic of implementing business logic and introduces a pattern oriented for complicated business logic: the domain model pattern.
与事务脚本和活动记录模式一样,域模型模式最初是在 Martin Fowler 的《企业应用程序架构模式》一书中引入的。Fowler 在结束对模式的讨论时说:“Eric Evans 目前正在写一本关于构建领域模型的书。” 参考书是埃文斯的开创性著作,领域驱动设计:解决软件核心的复杂性问题。
As with both the transaction script and active record patterns, the domain model pattern was introduced initially in Martin Fowler’s book Patterns of Enterprise Application Architecture. Fowler concluded his discussion of the pattern by saying, “Eric Evans is currently writing a book on building Domain Models.” The referenced book is Evans’s seminal work, Domain-Driven Design: Tackling Complexity in the Heart of Software.
在他的书中,Evans 提出了一组旨在将代码与业务领域的底层模型紧密相关的模式:聚合、值对象、存储库等。这些模式紧跟 Fowler 在他的书中离开的地方,类似于一组用于实现领域模型模式的有效工具。
In his book, Evans presents a set of patterns aimed at tightly relating the code to the underlying model of the business domain: aggregate, value objects, repositories, and others. These patterns closely follow where Fowler left off in his book and resemble an effective set of tools for implementing the domain model pattern.
Evans 引入的模式通常被称为战术领域驱动设计。为了消除认为实施领域驱动设计必然需要使用这些模式来实施业务逻辑的思维混乱,我更愿意坚持使用 Fowler 的原始术语。该模式是“领域模型”,聚合和值对象是其构建块。
The patterns that Evans introduced are often referred to as tactical domain-driven design. To eliminate the confusion of thinking that implementing domain-driven design necessarily entails the use of these patterns to implement business logic, I prefer to stick with Fowler’s original terminology. The pattern is “domain model,” and the aggregates and value objects are its building blocks.
领域模型模式旨在应对复杂的情况商业逻辑。在这里,我们处理的不是 CRUD 接口,而是复杂的状态转换、业务规则和不变量:必须始终受到保护的规则。
The domain model pattern is intended to cope with cases of complex business logic. Here, instead of CRUD interfaces, we deal with complicated state transitions, business rules, and invariants: rules that have to be protected at all times.
假设我们正在实施一个帮助台系统。考虑以下描述控制支持票生命周期逻辑的要求的摘录:
Let’s assume we are implementing a help desk system. Consider the following excerpt from the requirements that describes the logic controlling the lifecycles of support tickets:
客户打开支持票,描述他们面临的问题。
Customers open support tickets describing issues they are facing.
客户和支持代理都附加消息,所有通信都由支持票跟踪。
Both the customer and the support agent append messages, and all the correspondence is tracked by the support ticket.
每张工单都有一个优先级:低、中、高或紧急。
Each ticket has a priority: low, medium, high, or urgent.
代理应根据工单的优先级在设定的时限 (SLA) 内提供解决方案。
An agent should offer a solution within a set time limit (SLA) that is based on the ticket’s priority.
如果代理未在 SLA 内回复,客户可以将工单上报给代理的经理。
If the agent doesn’t reply within the SLA, the customer can escalate the ticket to the agent’s manager.
升级将代理的响应时间限制减少了 33%。
Escalation reduces the agent’s response time limit by 33%.
如果代理未在响应时间限制的 50% 内打开升级的工单,则会自动将其重新分配给其他代理。
If the agent didn’t open an escalated ticket within 50% of the response time limit, it is automatically reassigned to a different agent.
如果客户在 7 天内未回复代理的问题,工单将自动关闭。
Tickets are automatically closed if the customer doesn’t reply to the agent’s questions within seven days.
升级的工单不能自动或由代理关闭,只能由客户或代理的经理关闭。
Escalated tickets cannot be closed automatically or by the agent, only by the customer or the agent’s manager.
客户只有在过去 7 天内关闭的情况下才能重新打开已关闭的工单。
A customer can reopen a closed ticket only if it was closed in the past seven days.
这些要求在不同规则之间形成了一张错综复杂的依赖关系网,所有这些都会影响支持票证的生命周期管理逻辑。正如我们在上一章中讨论的那样,这不是一个 CRUD 数据输入屏幕。尝试使用活动记录对象实现此逻辑将很容易复制逻辑并通过错误实现某些业务规则来破坏系统状态。
These requirements form an entangled net of dependencies among the different rules, all affecting the support ticket’s lifecycle management logic. This is not a CRUD data entry screen, as we discussed in the previous chapter. Attempting to implement this logic using active record objects will make it easy to duplicate the logic and corrupt the system’s state by misimplementing some of the business rules.
域模型是包含以下内容的域的对象模型行为和数据。1 DDD 的战术模式——聚合、值对象、领域事件和领域服务——是此类对象模型的构建块。2个
A domain model is an object model of the domain that incorporates both behavior and data.1 DDD’s tactical patterns—aggregates, value objects, domain events, and domain services—are the building blocks of such an object model.2
所有这些模式都有一个共同的主题:它们将业务逻辑放在首位。让我们看看域模型如何解决不同的设计问题。
All of these patterns share a common theme: they put the business logic first. Let’s see how the domain model addresses different design concerns.
域的业务逻辑本来就很复杂,所以对象用于建模它不应该引入任何额外的意外复杂性。该模型应该没有任何基础设施或技术问题,例如实现对数据库或系统其他外部组件的调用。此限制要求模型的对象是普通的旧对象,即在不依赖或直接合并任何基础结构组件或框架的情况下实现业务逻辑的对象。3个
The domain’s business logic is already inherently complex, so the objects used for modeling it should not introduce any additional accidental complexities. The model should be devoid of any infrastructural or technological concerns, such as implementing calls to databases or other external components of the system. This restriction requires the model’s objects to be plain old objects, objects implementing business logic without relying on or directly incorporating any infrastructural components or frameworks.3
强调业务逻辑而不是技术问题使得领域模型的对象更容易遵循限界上下文的通用语言的术语。换句话说,这种模式允许代码“说”通用语言并遵循领域专家的心智模型。
The emphasis on business logic instead of technical concerns makes it easier for the domain model’s objects to follow the terminology of the bounded context’s ubiquitous language. In other words, this pattern allows the code to “speak” the ubiquitous language and to follow the domain experts’ mental models.
让我们看看 DDD 提供的中心域模型构建块或战术模式:值对象、聚合和域服务。
Let’s look at the central domain model building blocks, or tactical patterns, offered by DDD: value objects, aggregates, and domain services.
值对象是可以通过其值的组合来标识的对象。例如,考虑一个颜色对象:
A value object is an object that can be identified by the composition of its values. For example, consider a color object:
classColor{int_red;int_green;int_blue;}
classColor{int_red;int_green;int_blue;}
红色、绿色和蓝色三个字段的值的组合定义了一种颜色。更改其中一个字段的值将产生新的颜色。没有两种颜色可以具有相同的值。此外,相同颜色的两个实例必须具有相同的值。因此,不需要明确的识别字段来识别颜色。
The composition of the values of the three fields red, green, and blue defines a color. Changing the value of one of the fields will result in a new color. No two colors can have the same values. Also, two instances of the same color must have the same values. Therefore, no explicit identification field is needed to identify colors.
图 6-1ColorId中显示的字段不仅是多余的,而且实际上为错误创造了机会。您可以创建具有相同 、 和 值的两行,但比较 的值不会反映这是相同的颜色。redgreenblueColorId
The ColorId field shown in Figure 6-1 is not only redundant, but actually creates an opening for bugs. You could create two rows with the same values of red, green, and blue, but comparing the values of ColorId would not reflect that this is the same color.
ColorId字段,使两行具有相同的值成为可能完全依赖语言的标准库的原语表示业务领域概念的数据类型(如字符串、整数或字典)被称为原始痴迷4代码味道。例如,考虑以下类:
Relying exclusively on the language’s standard library’s primitive data types—such as strings, integers, or dictionaries—to represent concepts of the business domain is known as the primitive obsession4 code smell. For example, consider the following class:
classPerson{privateint_id;privatestring_firstName;privatestring_lastName;privatestring_landlinePhone;privatestring_mobilePhone;privatestring_email;privateint_heightMetric;privatestring_countryCode;publicPerson(...){...}}staticvoidMain(string[]args){vardave=newPerson(id:30217,firstName:"Dave",lastName:"Ancelovici",landlinePhone:"023745001",mobilePhone:"0873712503",:"dave@learning-ddd.com",heightMetric:180,countryCode:"BG");}
classPerson{privateint_id;privatestring_firstName;privatestring_lastName;privatestring_landlinePhone;privatestring_mobilePhone;privatestring_email;privateint_heightMetric;privatestring_countryCode;publicPerson(...){...}}staticvoidMain(string[]args){vardave=newPerson(id:30217,firstName:"Dave",lastName:"Ancelovici",landlinePhone:"023745001",mobilePhone:"0873712503",:"dave@learning-ddd.com",heightMetric:180,countryCode:"BG");}
在类的前面实现中Person,大多数值都是类型的String,并且它们是根据约定分配的。例如,输入的landlinePhone应该是一个有效的固定电话号码,而输入countryCode应该是一个有效的、两个字母的大写国家代码。当然,系统不能相信用户总是提供正确的值,因此,该类必须验证所有输入字段。
In the preceding implementation of the Person class, most of the values are of type String and they are assigned based on convention. For example, the input to the landlinePhone should be a valid landline phone number, and the countryCode should be a valid, two-letter, uppercased country code. Of course, the system cannot trust the user to always supply correct values, and as a result, the class has to validate all input fields.
这种方法存在多种设计风险。首先,验证逻辑往往是重复的。其次,很难在使用值之前强制调用验证逻辑。将来,当代码库将由其他工程师开发时,它将变得更具挑战性。
This approach presents multiple design risks. First, the validation logic tends to be duplicated. Second, it’s hard to enforce calling the validation logic before the values are used. It will become even more challenging in the future, when the codebase will be evolved by other engineers.
比较同一对象的以下替代设计,这次利用值对象:
Compare the following alternative design of the same object, this time leveraging value objects:
classPerson{privatePersonId_id;privateName_name;privatePhoneNumber_landline;privatePhoneNumber_mobile;privateEmailAddress_email;privateHeight_height;privateCountryCode_country;publicPerson(...){...}}staticvoidMain(string[]args){vardave=newPerson(id:newPersonId(30217),name:newName("Dave","Ancelovici"),landline:PhoneNumber.Parse("023745001"),mobile:PhoneNumber.Parse("0873712503"),:.Parse("dave@learning-ddd.com"),height:Height.FromMetric(180),country:CountryCode.Parse("BG"));}
classPerson{privatePersonId_id;privateName_name;privatePhoneNumber_landline;privatePhoneNumber_mobile;privateEmailAddress_email;privateHeight_height;privateCountryCode_country;publicPerson(...){...}}staticvoidMain(string[]args){vardave=newPerson(id:newPersonId(30217),name:newName("Dave","Ancelovici"),landline:PhoneNumber.Parse("023745001"),mobile:PhoneNumber.Parse("0873712503"),:.Parse("dave@learning-ddd.com"),height:Height.FromMetric(180),country:CountryCode.Parse("BG"));}
首先,请注意清晰度的提高。以变量为例country。没有必要精心称呼它为“国家代码”来传达它持有国家代码而不是完整国家名称的意图。值对象使意图清晰,即使使用较短的变量名称。
First, notice the increased clarity. Take, for example, the country variable. There is no need to elaborately call it “countryCode” to communicate the intent of it holding a country code and not, for example, a full country name. The value object makes the intent clear, even with shorter variable names.
其次,不需要在赋值之前验证值,因为验证逻辑驻留在值对象本身中。然而,值对象的行为并不仅限于验证。当值对象集中处理值的业务逻辑时,它们最闪耀。内聚逻辑在一处实现,易于测试。最重要的是,值对象表达业务领域的概念:它们使代码使用通用语言。
Second, there is no need to validate the values before the assignment, as the validation logic resides in the value objects themselves. However, a value object’s behavior is not limited to mere validation. Value objects shine brightest when they centralize the business logic that manipulates the values. The cohesive logic is implemented in one place and is easy to test. Most importantly, value objects express the business domain’s concepts: they make the code speak the ubiquitous language.
让我们看看将身高、电话号码和颜色的概念表示为值对象如何使生成的类型系统丰富且易于使用。
Let’s see how representing the concepts of height, phone numbers, and colors as value objects makes the resultant type system rich and intuitive to use.
与基于整数的值相比,Height值对象既使意图清晰,又将度量与特定度量单位解耦。例如,Height值对象可以使用公制和英制单位进行初始化,从而可以轻松地将一种单位转换为另一种单位、生成字符串表示形式以及比较不同单位的值:
Compared to an integer-based value, the Height value object both makes the intent clear and decouples the measurement from a specific measurement unit. For example, the Height value object can be initialized using both metric and imperial units, making it easy to convert from one unit to another, generating string representation, and comparing values of different units:
varheightMetric=Height.Metric(180);varheightImperial=Height.Imperial(5,3);varstring1=heightMetric.ToString();// "180cm"varstring2=heightImperial.ToString();// "5 feet 3 inches"varstring3=heightMetric.ToImperial().ToString();// "5 feet 11 inches"varfirstIsHigher=heightMetric>heightImperial;// true
varheightMetric=Height.Metric(180);varheightImperial=Height.Imperial(5,3);varstring1=heightMetric.ToString();// "180cm"varstring2=heightImperial.ToString();// "5 feet 3 inches"varstring3=heightMetric.ToImperial().ToString();// "5 feet 11 inches"varfirstIsHigher=heightMetric>heightImperial;// true
值对象PhoneNumber可以封装字符串值的解析、验证、提取电话号码不同属性的逻辑;例如,它所属的国家和电话号码的类型——固定电话或手机:
The PhoneNumber value object can encapsulate the logic of parsing a string value, validating it, and extracting different attributes of the phone number; for example, the country it belongs to and the phone number’s type—landline or mobile:
varphone=PhoneNumber.Parse("+359877123503");varcountry=phone.Country;// "BG"varphoneType=phone.PhoneType;// "MOBILE"varisValid=PhoneNumber.IsValid("+972120266680");// false
varphone=PhoneNumber.Parse("+359877123503");varcountry=phone.Country;// "BG"varphoneType=phone.PhoneType;// "MOBILE"varisValid=PhoneNumber.IsValid("+972120266680");// false
以下示例演示了值对象封装所有操作数据的业务逻辑并生成值对象的新实例时的强大功能:
The following example demonstrates the power of a value object when it encapsulates all of the business logic that manipulates the data and produces new instances of the value object:
varred=Color.FromRGB(255,0,0);vargreen=Color.Green;varyellow=red.MixWith(green);varyellowString=yellow.ToString();// "#FFFF00"
varred=Color.FromRGB(255,0,0);vargreen=Color.Green;varyellow=red.MixWith(green);varyellowString=yellow.ToString();// "#FFFF00"
正如您在前面的示例中看到的那样,值对象消除了约定的需要——例如,需要记住这个字符串是电子邮件而另一个字符串是电话号码——而是使用对象模型减少了错误容易且更直观。
As you can see in the preceding examples, value objects eliminate the need for conventions—for example, the need to keep in mind that this string is an email and the other string is a phone number—and instead makes using the object model less error prone and more intuitive.
由于对值对象的任何字段的更改都会导致不同的值,值对象被实现为不可变对象。对值对象的其中一个字段的更改在概念上会创建一个不同的值——值对象的一个不同实例。因此,当执行的操作产生新值时,如以下使用该MixWith方法的情况,它不会修改原始实例,而是实例化并返回一个新值:
Since a change to any of the fields of a value object results in a different value, value objects are implemented as immutable objects. A change to one of the value object’s fields conceptually creates a different value—a different instance of a value object. Therefore, when an executed action results in a new value, as in the following case, which uses the MixWith method, it doesn’t modify the original instance but instantiates and returns a new one:
publicclassColor{publicreadonlybyteRed;publicreadonlybyteGreen;publicreadonlybyteBlue;publicColor(byter,byteg,byteb){this.Red=r;this.Green=g;this.Blue=b;}publicColorMixWith(Colorother){returnnewColor(r:(byte)Math.Min(this.Red+other.Red,255),g:(byte)Math.Min(this.Green+other.Green,255),b:(byte)Math.Min(this.Blue+other.Blue,255));}...}
publicclassColor{publicreadonlybyteRed;publicreadonlybyteGreen;publicreadonlybyteBlue;publicColor(byter,byteg,byteb){this.Red=r;this.Green=g;this.Blue=b;}publicColorMixWith(Colorother){returnnewColor(r:(byte)Math.Min(this.Red+other.Red,255),g:(byte)Math.Min(this.Green+other.Green,255),b:(byte)Math.Min(this.Blue+other.Blue,255));}...}
由于值对象的相等性基于它们的值而不是基于字段id或引用,因此重写并正确实施相等性检查很重要。例如,在 C# 中:5
Since the equality of value objects is based on their values rather than on an id field or reference, it’s important to override and properly implement the equality checks. For example, in C#:5
publicclassColor{...publicoverrideboolEquals(objectobj){varother=objasColor;returnother!=null&&this.Red==other.Red&&this.Green==other.Green&&this.Blue==other.Blue;}publicstaticbooloperator==(Colorlhs,Colorrhs){if(Object.ReferenceEquals(lhs,null)){returnObject.ReferenceEquals(rhs,null);}returnlhs.Equals(rhs);}publicstaticbooloperator!=(Colorlhs,Colorrhs){return!(lhs==rhs);}publicoverrideintGetHashCode(){returnToString().GetHashCode();}...}
publicclassColor{...publicoverrideboolEquals(objectobj){varother=objasColor;returnother!=null&&this.Red==other.Red&&this.Green==other.Green&&this.Blue==other.Blue;}publicstaticbooloperator==(Colorlhs,Colorrhs){if(Object.ReferenceEquals(lhs,null)){returnObject.ReferenceEquals(rhs,null);}returnlhs.Equals(rhs);}publicstaticbooloperator!=(Colorlhs,Colorrhs){return!(lhs==rhs);}publicoverrideintGetHashCode(){returnToString().GetHashCode();}...}
尽管使用核心库的Strings 来表示特定于域的值与值对象的概念相矛盾,但在 .NET、Java 和其他语言中,字符串类型完全作为值对象实现。字符串是不可变的,因为所有操作都会产生一个新实例。此外,字符串类型封装了丰富的行为,通过操作一个或多个字符串的值来创建新的实例:修剪、连接多个字符串、替换字符、子字符串等方法。
Although using a core library’s Strings to represent domain-specific values contradicts the notion of value objects, in .NET, Java, and other languages the string type is implemented exactly as a value object. Strings are immutable, as all operations result in a new instance. Moreover, the string type encapsulates a rich behavior that creates new instances by manipulating the values of one or more strings: trim, concatenate multiple strings, replace characters, substring, and other methods.
简单的答案是,只要有可能。不仅值对象使代码更具表现力并封装易于分散的业务逻辑,但该模式使代码更安全。由于值对象是不可变的,因此值对象的行为没有副作用并且是线程安全的。
The simple answer is, whenever you can. Not only do value objects make the code more expressive and encapsulate business logic that tends to spread apart, but the pattern makes the code safer. Since value objects are immutable, the value objects’ behavior is free of side effects and is thread safe.
从业务领域的角度来看,一个有用的经验法则是对描述其他对象属性的领域元素使用值对象。这适用于实体的属性,将在下一节中讨论。您之前看到的示例使用值对象来描述一个人,包括他们的 ID、姓名、电话号码、电子邮件等。使用值对象的其他示例包括各种状态、密码和更多业务领域特定的概念,这些概念可以通过它们的值来识别,因此不需要明确的识别字段。引入价值对象的一个特别重要的机会是在为货币和其他货币价值建模时。依靠原始类型来表示货币不仅限制了您将所有与货币相关的业务逻辑封装在一个地方的能力,
From a business domain perspective, a useful rule of thumb is to use value objects for the domain’s elements that describe properties of other objects. This namely applies to properties of entities, which are discussed in the next section. The examples you saw earlier used value objects to describe a person, including their ID, name, phone numbers, email, and so on. Other examples of using value objects include various statuses, passwords, and more business domain–specific concepts that can be identified by their values and thus do not require an explicit identification field. An especially important opportunity to introduce a value object is when modeling money and other monetary values. Relying on primitive types to represent money not only limits your ability to encapsulate all money-related business logic in one place, but also often leads to dangerous bugs, such as rounding errors and other precision-related issues.
实体是值对象的对立面。它需要一个明确的标识字段来区分实体的不同实例。实体的一个简单例子是人。考虑以下课程:
An entity is the opposite of a value object. It requires an explicit identification field to distinguish between the different instances of the entity. A trivial example of an entity is a person. Consider the following class:
classPerson{publicNameName{get;set;}publicPerson(Namename){this.Name=name;}}
classPerson{publicNameName{get;set;}publicPerson(Namename){this.Name=name;}}
该类仅包含一个字段:(name一个值对象)。然而,这种设计并不是最理想的,因为不同的人可以同名并且可以有完全相同的名字。当然,这并不能使他们成为同一个人。因此,需要一个识别字段来正确识别人:
The class contains only one field: name (a value object). This design, however, is suboptimal because different people can be namesakes and can have exactly the same names. That, of course, doesn’t make them the same person. Hence, an identification field is needed to properly identify people:
classPerson{publicreadonlyPersonIdId;publicNameName{get;set;}publicPerson(PersonIdid,Namename){this.Id=id;this.Name=name;}}
classPerson{publicreadonlyPersonIdId;publicNameName{get;set;}publicPerson(PersonIdid,Namename){this.Id=id;this.Name=name;}}
在前面的代码中,我们引入了标识字段 Id的类型PersonId。PersonId是一个值对象,它可以使用适合业务领域需要的任何底层数据类型。例如,Id可以是 GUID、数字、字符串或特定于域的值(如社会保险号)。
In the preceding code, we introduced the identification field Id of type PersonId. PersonId is a value object, and it can use any underlying data types that fit the business domain’s needs. For example, the Id can be a GUID, a number, a string, or a domain-specific value such as a Social Security number.
标识字段的核心要求是它对于实体的每个实例都应该是唯一的:对于每个人,在我们的例子中(图 6-2)。此外,除了非常罕见的例外,实体标识字段的值在实体的整个生命周期中应该保持不变。这将我们带到值对象和实体之间的第二个概念差异。
The central requirement for the identification field is that it should be unique for each instance of the entity: for each person, in our case (Figure 6-2). Furthermore, except for very rare exceptions, the value of an entity’s identification field should remain immutable throughout the entity’s lifecycle. This brings us to the second conceptual difference between value objects and entities.
与值对象相反,实体不是不可变的,而是会发生变化的。实体和值对象之间的另一个区别是值对象描述实体的属性。在本章的前面,您看到了一个实体示例Person,它有两个值对象来描述每个实例:PersonId和Name。
Contrary to value objects, entities are not immutable and are expected to change. Another difference between entities and value objects is that value objects describe an entity’s properties. Earlier in the chapter, you saw an example of the entity Person and it had two value objects describing each instance: PersonId and Name.
实体是任何业务领域的基本构建块。也就是说,您可能已经注意到,在本章的前面,我没有将“实体”包括在域模型构建块的列表中。那不是错误。省略“实体”的原因是因为我们不是独立地实现实体,而是仅在聚合模式的上下文中实现。
Entities are an essential building block of any business domain. That said, you may have noticed that earlier in the chapter I didn’t include “entity” in the list of the domain model’s building blocks. That’s not a mistake. The reason “entity” was omitted is because we don’t implement entities independently, but only in the context of the aggregate pattern.
聚合是一个实体:它需要一个明确的标识字段及其状态预计会在实例的生命周期中发生变化。然而,它不仅仅是一个实体。该模式的目标是保护其数据的一致性。由于聚合的数据是可变的,因此模式必须解决一些影响和挑战,以始终保持其状态一致。
An aggregate is an entity: it requires an explicit identification field and its state is expected to change during an instance’s lifecycle. However, it is much more than just an entity. The goal of the pattern is to protect the consistency of its data. Since an aggregate’s data is mutable, there are implications and challenges that the pattern has to address to keep its state consistent at all times.
由于聚合的状态可以改变,因此它创建了一个开口其数据可能被破坏的多种方式。为了强制数据的一致性,聚合模式在聚合与其外部范围之间划定了明确的边界:聚合是一个一致性强制边界。聚合的逻辑必须验证所有传入的修改并确保这些更改不与其业务规则相矛盾。
Since an aggregate’s state can be mutated, it creates an opening for multiple ways in which its data can become corrupted. To enforce consistency of the data, the aggregate pattern draws a clear boundary between the aggregate and its outer scope: the aggregate is a consistency enforcement boundary. The aggregate’s logic has to validate all incoming modifications and ensure that the changes do not contradict its business rules.
从实现的角度来看,一致性是通过只允许聚合的业务逻辑修改其状态来实现的。聚合外部的所有进程或对象只允许读取聚合的状态。它的状态只能通过执行聚合公共接口的相应方法来改变。
From an implementation perspective, the consistency is enforced by allowing only the aggregate’s business logic to modify its state. All processes or objects external to the aggregate are only allowed to read the aggregate’s state. Its state can only be mutated by executing corresponding methods of the aggregate’s public interface.
作为聚合的公共接口公开的状态修改方法通常被称为命令,如“做某事的命令”。命令可以通过两种方式实现。首先,它可以作为聚合对象的普通公共方法来实现:
The state-modifying methods exposed as an aggregate’s public interface are often referred to as commands, as in “a command to do something.” A command can be implemented in two ways. First, it can be implemented as a plain public method of the aggregate object:
publicclassTicket{...publicvoidAddMessage(UserIdfrom,stringbody){varmessage=newMessage(from,body);_messages.Append(message);}...}
publicclassTicket{...publicvoidAddMessage(UserIdfrom,stringbody){varmessage=newMessage(from,body);_messages.Append(message);}...}
或者,命令可以表示为一个参数对象,它封装了执行命令所需的所有输入:
Alternatively, a command can be represented as a parameter object that encapsulates all the input required for executing the command:
publicclassTicket{...publicvoidExecute(AddMessagecmd){varmessage=newMessage(cmd.from,cmd.body);_messages.Append(message);}...}
publicclassTicket{...publicvoidExecute(AddMessagecmd){varmessage=newMessage(cmd.from,cmd.body);_messages.Append(message);}...}
如何在聚合代码中表达命令是一个偏好问题。我更喜欢定义命令结构并将它们以多态方式传递给相关Execute方法的更明确的方式。
How commands are expressed in an aggregate’s code is a matter of preference. I prefer the more explicit way of defining command structures and passing them polymorphically to the relevant Execute method.
聚合的公共接口负责验证输入并执行所有相关业务规则和不变量。这种严格的边界还确保与聚合相关的所有业务逻辑都在一个地方实现:聚合本身。
An aggregate’s public interface is responsible for validating the input and enforcing all of the relevant business rules and invariants. This strict boundary also ensures that all business logic related to the aggregate is implemented in one place: the aggregate itself.
这使得在聚合上编排操作的应用层6相当简单:7它所要做的就是加载聚合的当前状态,执行所需的操作,持久保存修改后的状态,并将操作的结果返回给调用者:
This makes the application layer6 that orchestrates operations on aggregates rather simple:7 all it has to do is load the aggregate’s current state, execute the required action, persist the modified state, and return the operation’s result to the caller:
01publicExecutionResultEscalate(TicketIdid,EscalationReasonreason)02{03try04{05varticket=_ticketRepository.Load(id);06varcmd=newEscalate(reason);07ticket.Execute(cmd);08_ticketRepository.Save(ticket);09returnExecutionResult.Success();10}11catch(ConcurrencyExceptionex)12{13returnExecutionResult.Error(ex);14}15}
01publicExecutionResultEscalate(TicketIdid,EscalationReasonreason)02{03try04{05varticket=_ticketRepository.Load(id);06varcmd=newEscalate(reason);07ticket.Execute(cmd);08_ticketRepository.Save(ticket);09returnExecutionResult.Success();10}11catch(ConcurrencyExceptionex)12{13returnExecutionResult.Error(ex);14}15}
请注意上述代码(第 11 行)中的并发检查。保护聚合状态的一致性至关重要。8如果多个进程同时更新同一个聚合,我们必须防止后一个事务盲目地覆盖第一个事务提交的更改。在这种情况下,必须通知第二个进程其决策所依据的状态已过时,并且必须重试该操作。
Pay attention to the concurrency check in the preceding code (line 11). It’s vital to protect the consistency of an aggregate’s state.8 If multiple processes are concurrently updating the same aggregate, we have to prevent the latter transaction from blindly overwriting the changes committed by the first one. In such a case, the second process has to be notified that the state on which it had based its decisions is out of date, and it has to retry the operation.
因此,用于存储聚合的数据库必须支持并发管理。在其最简单的形式中,聚合应该包含一个版本字段,该字段将在每次更新后递增:
Hence, the database used for storing aggregates has to support concurrency management. In its simplest form, an aggregate should hold a version field that will be incremented after each update:
classTicket{TicketId_id;int_version;...}
classTicket{TicketId_id;int_version;...}
当提交对数据库的更改时,我们必须确保被覆盖的版本与最初读取的版本相匹配。例如,在 SQL 中:
When committing a change to the database, we have to ensure that the version that is being overwritten matches the one that was originally read. For example, in SQL:
01UPDATEtickets02SETticket_status=@new_status,03agg_version=agg_version+104WHEREticket_id=@idandagg_version=@expected_version;
01UPDATEtickets02SETticket_status=@new_status,03agg_version=agg_version+104WHEREticket_id=@idandagg_version=@expected_version;
此 SQL 语句应用对聚合实例状态所做的更改(第 2 行),并增加其版本计数器(第 3 行),但前提是当前版本等于在应用更改聚合状态(第 4 行)之前读取的版本。
This SQL statement applies changes made to the aggregate instance’s state (line 2), and increases its version counter (line 3) but only if the current version equals the one that was read prior to applying changes to the aggregate’s state (line 4).
当然,并发管理可以在关系数据库之外的其他地方实现。此外,文档数据库更适合使用聚合。也就是说,确保用于存储聚合数据的数据库支持并发管理至关重要。
Of course, concurrency management can be implemented elsewhere besides a relational database. Furthermore, document databases lend themselves more toward working with aggregates. That said, it’s crucial to ensure that the database used for storing an aggregate’s data supports concurrency management.
由于聚合的状态只能由其自身的业务逻辑修改,聚合也充当事务边界。对聚合状态的所有更改都应作为一个原子操作以事务方式提交。如果聚合的状态被修改,则要么提交所有更改,要么都不提交。
Since an aggregate’s state can only be modified by its own business logic, the aggregate also acts as a transactional boundary. All changes to the aggregate’s state should be committed transactionally as one atomic operation. If an aggregate’s state is modified, either all the changes are committed or none of them is.
此外,没有任何系统操作可以假设多聚合事务。对聚合状态的更改只能单独提交,每个数据库事务一个聚合。
Furthermore, no system operation can assume a multi-aggregate transaction. A change to an aggregate’s state can only be committed individually, one aggregate per database transaction.
每个事务一个聚合实例迫使我们仔细设计聚合的边界,确保设计解决业务领域的不变量和规则。需要在多个聚合中提交更改表示错误的事务边界,因此也是错误的聚合边界。
The one aggregate instance per transaction forces us to carefully design an aggregate’s boundaries, ensuring that the design addresses the business domain’s invariants and rules. The need to commit changes in multiple aggregates signals a wrong transaction boundary, and hence, wrong aggregate boundaries.
这似乎强加了建模限制。如果我们需要在同一个事务中修改多个对象怎么办?让我们看看该模式如何解决此类情况。
This seems to impose a modeling limitation. What if we need to modify multiple objects in the same transaction? Let’s see how the pattern addresses such situations.
正如我们在本章前面所讨论的,我们不使用实体作为一个独立的模式,仅作为聚合的一部分。让我们看看实体和聚合之间的根本区别,以及为什么实体是聚合的构建块而不是总体域模型的构建块。
As we discussed earlier in the chapter, we don’t use entities as an independent pattern, only as part of an aggregate. Let’s see the fundamental difference between entities and aggregates, and why entities are a building block of an aggregate rather than of the overarching domain model.
在某些业务场景中,多个对象应该共享一个事务边界;例如,当两者可以同时修改或一个对象的业务规则取决于另一个对象的状态时。
There are business scenarios in which multiple objects should share a transactional boundary; for example, when both can be modified simultaneously or the business rules of one object depend on the state of another object.
DDD 规定系统的设计应由其业务领域驱动。聚合也不例外。为了支持必须在一个原子事务中应用的多个对象的更改,聚合模式类似于实体的层次结构,所有共享事务一致性,如图 6-3所示。
DDD prescribes that a system’s design should be driven by its business domain. Aggregates are no exception. To support changes to multiple objects that have to be applied in one atomic transaction, the aggregate pattern resembles a hierarchy of entities, all sharing transactional consistency, as shown in Figure 6-3.
层次结构包含实体和值对象,如果它们受域的业务逻辑约束,则它们都属于同一个聚合。
The hierarchy contains both entities and value objects, and all of them belong to the same aggregate if they are bound by the domain’s business logic.
这就是该模式被命名为“聚合”的原因:它聚合了属于同一事务边界的业务实体和值对象。
That’s why the pattern is named “aggregate”: it aggregates business entities and value objects that belong to the same transaction boundary.
以下代码示例演示了跨越属于聚合边界的多个实体的业务规则——“如果代理未在响应时间限制的 50% 内打开升级工单,它会自动重新分配给不同的代理”:
The following code sample demonstrates a business rule that spans multiple entities belonging to the aggregate’s boundary—“If an agent didn’t open an escalated ticket within 50% of the response time limit, it is automatically reassigned to a different agent”:
01publicclassTicket02{03...04List<Message>_messages;05...0607publicvoidExecute(EvaluateAutomaticActionscmd)08{09if(this.IsEscalated&&this.RemainingTimePercentage<0.5&&10GetUnreadMessagesCount(for:AssignedAgent)>0)11{12_agent=AssignNewAgent();13}14}1516publicintGetUnreadMessagesCount(UserIdid)17{18return_messages.Where(x=>x.To==id&&!x.WasRead).Count();19}2021...22}
01publicclassTicket02{03...04List<Message>_messages;05...0607publicvoidExecute(EvaluateAutomaticActionscmd)08{09if(this.IsEscalated&&this.RemainingTimePercentage<0.5&&10GetUnreadMessagesCount(for:AssignedAgent)>0)11{12_agent=AssignNewAgent();13}14}1516publicintGetUnreadMessagesCount(UserIdid)17{18return_messages.Where(x=>x.To==id&&!x.WasRead).Count();19}2021...22}
该方法检查工单的值以查看它是否升级以及剩余处理时间是否小于定义的阈值 50%(第 9 行)。此外,它还会检查当前代理尚未读取的消息(第 10 行)。如果满足所有条件,则请求将票重新分配给不同的代理。
The method checks the ticket’s values to see whether it is escalated and whether the remaining processing time is less than the defined threshold of 50% (line 9). Furthermore, it checks for messages that were not yet read by the current agent (line 10). If all conditions are met, the ticket is requested to be reassigned to a different agent.
聚合确保所有条件都根据高度一致的数据进行检查,并且通过确保对聚合数据的所有更改都作为一个原子事务执行,在检查完成后它不会更改。
The aggregate ensures that all the conditions are checked against strongly consistent data, and it won’t change after the checks are completed by ensuring that all changes to the aggregate’s data are performed as one atomic transaction.
由于聚合包含的所有对象共享相同的如果聚合增长太大,可能会出现事务边界、性能和可伸缩性问题。
Since all objects contained by an aggregate share the same transactional boundary, performance and scalability issues may arise if an aggregate grows too large.
数据的一致性可以成为设计聚合边界的方便指导原则。只有聚合的业务逻辑要求高度一致的信息才应该是聚合的一部分。所有最终一致的信息都应该位于聚合边界之外;例如,作为另一个聚合的一部分,如图6-4所示。
The consistency of the data can be a convenient guiding principle for designing an aggregate’s boundaries. Only the information that is required by the aggregate’s business logic to be strongly consistent should be a part of the aggregate. All information that can be eventually consistent should reside outside of the aggregate’s boundary; for example, as a part of another aggregate, as shown in Figure 6-4.
经验法则是使聚合尽可能小,并且仅包含聚合的业务逻辑要求处于高度一致状态的对象:
The rule of thumb is to keep the aggregates as small as possible and include only objects that are required to be in a strongly consistent state by the aggregate’s business logic:
publicclassTicket{privateUserId_customer;privateList<ProductId>_products;privateUserId_assignedAgent;privateList<Message>_messages;...}
publicclassTicket{privateUserId_customer;privateList<ProductId>_products;privateUserId_assignedAgent;privateList<Message>_messages;...}
在前面的示例中,Ticket聚合引用了属于聚合边界的消息集合。另一方面,客户、与工单相关的产品集合以及分配的代理不属于聚合,因此由其 ID 引用。
In the preceding example, the Ticket aggregate references a collection of messages, which belong to the aggregate’s boundary. On the other hand, the customer, the collection of products that are relevant to the ticket, and the assigned agent do not belong to the aggregate and therefore are referenced by its ID.
通过 ID 引用外部聚合背后的原因是为了具体化这些对象不属于聚合的边界,并确保每个聚合都有自己的事务边界。
The reasoning behind referencing external aggregates by ID is to reify that these objects do not belong to the aggregate’s boundary, and to ensure that each aggregate has its own transactional boundary.
要确定一个实体是否属于聚合,请检查聚合是否包含可能导致无效系统状态的业务逻辑(如果它处理最终一致的数据)。让我们回到前面的示例,如果当前代理未在响应时间限制的 50% 内阅读新消息,则重新分配工单。如果有关已读/未读消息的信息最终是一致的怎么办?换句话说,在一定延迟之后接收阅读确认是合理的。在这种情况下,可以安全地预期相当多的票证会被不必要地重新分配。那当然会破坏系统的状态。因此,消息中的数据属于聚合的边界。
To decide whether an entity belongs to an aggregate or not, examine whether the aggregate contains business logic that can lead to an invalid system state if it will work on eventually consistent data. Let’s go back to the previous example of reassigning the ticket if the current agent didn’t read the new messages within 50% of the response time limit. What if the information about read/unread messages would be eventually consistent? In other words, it would be reasonable to receive reading acknowledgment after a certain delay. In that case, it’s safe to expect a considerable number of tickets to be unnecessarily reassigned. That, of course, would corrupt the system’s state. Therefore, the data in the messages belongs to the aggregate’s boundary.
我们之前看到聚合的状态只能被修改通过执行它的命令之一。由于聚合表示实体的层次结构,因此只有其中一个实体应该被指定为聚合的公共接口——聚合根,如图6-5所示。
We saw earlier that an aggregate’s state can only be modified by executing one of its commands. Since an aggregate represents a hierarchy of entities, only one of them should be designated as the aggregate’s public interface—the aggregate root, as shown in Figure 6-5.
考虑以下聚合摘录Ticket:
Consider the following excerpt of the Ticket aggregate:
publicclassTicket{...List<Message>_messages;...publicvoidExecute(AcknowledgeMessagecmd){varmessage=_messages.Where(x=>x.Id==cmd.id).First();message.WasRead=true;}...}
publicclassTicket{...List<Message>_messages;...publicvoidExecute(AcknowledgeMessagecmd){varmessage=_messages.Where(x=>x.Id==cmd.id).First();message.WasRead=true;}...}
在此示例中,聚合公开了一个允许将特定消息标记为已读的命令。尽管该操作修改了实体的实例Message,但它只能通过其聚合根访问:Ticket。
In this example, the aggregate exposes a command that allows marking a specific message as read. Although the operation modifies an instance of the Message entity, it is accessible only through its aggregate root: Ticket.
除了聚合根的公共接口外,外部世界还可以通过另一种机制与聚合进行通信:领域事件。
In addition to the aggregate root’s public interface, there is another mechanism through which the outer world can communicate with aggregates: domain events.
A domain event is a message describing a significant event that has occurred in the business domain. For example:
已分配工单
Ticket assigned
工单升级
Ticket escalated
收到消息
Message received
由于领域事件描述的是已经发生的事情,因此它们的名称应该用过去时表示。
Since domain events describe something that has already happened, their names should be formulated in the past tense.
领域事件的目标是描述业务领域中发生的事情,并提供与事件相关的所有必要数据。例如,以下域事件表明特定工单已升级、升级时间和原因:
The goal of a domain event is to describe what has happened in the business domain and provide all the necessary data related to the event. For example, the following domain event communicates that the specific ticket was escalated, at what time, and for what reason:
{"ticket-id":"c9d286ff-3bca-4f57-94d4-4d4e490867d1","event-id":146,"event-type":"ticket-escalated","escalation-reason":"missed-sla","escalation-time":1628970815}
{"ticket-id":"c9d286ff-3bca-4f57-94d4-4d4e490867d1","event-id":146,"event-type":"ticket-escalated","escalation-reason":"missed-sla","escalation-time":1628970815}
与软件工程中的几乎所有事物一样,命名很重要。确保领域事件的名称简洁准确地反映了业务领域中发生的事情。
As with almost everything in software engineering, naming is important. Make sure the names of the domain events succinctly reflect exactly what has happened in the business domain.
领域事件是聚合公共接口的一部分。聚合发布其领域事件。其他进程、聚合甚至外部系统都可以订阅并执行自己的逻辑以响应领域事件,如图6-6所示。
Domain events are part of an aggregate’s public interface. An aggregate publishes its domain events. Other processes, aggregates, or even external systems can subscribe to and execute their own logic in response to the domain events, as shown in Figure 6-6.
在聚合的以下摘录中Ticket,一个新的领域事件被实例化(第 12 行)并附加到票证领域事件的集合中(第 13 行):
In the following excerpt from the Ticket aggregate, a new domain event is instantiated (line 12) and appended to the collection of the ticket’s domain events (line 13):
01publicclassTicket02{03...04privateList<DomainEvent>_domainEvents;05...0607publicvoidExecute(RequestEscalationcmd)08{09if(!this.IsEscalated&&this.RemainingTimePercentage<=0)10{11this.IsEscalated=true;12varescalatedEvent=newTicketEscalated(_id,cmd.Reason);13_domainEvents.Append(escalatedEvent);14}15}1617...18}
01publicclassTicket02{03...04privateList<DomainEvent>_domainEvents;05...0607publicvoidExecute(RequestEscalationcmd)08{09if(!this.IsEscalated&&this.RemainingTimePercentage<=0)10{11this.IsEscalated=true;12varescalatedEvent=newTicketEscalated(_id,cmd.Reason);13_domainEvents.Append(escalatedEvent);14}15}1617...18}
在第 9 章中,我们将讨论如何将领域事件可靠地发布给感兴趣的订阅者。
In Chapter 9, we will discuss how domain events can be reliably published to interested subscribers.
最后但同样重要的是,聚合应该反映无处不在的语言。用于聚合名称、它的数据成员、它的操作和它的域事件的术语都应该用限界上下文的通用语言来表述。正如 Eric Evans 所说,代码必须基于开发人员在与彼此和领域专家交谈时使用的相同语言。这对于实现复杂的业务逻辑尤为重要。
Last but not least, aggregates should reflect the ubiquitous language. The terminology that is used for the aggregate’s name, its data members, its actions, and its domain events all should be formulated in the bounded context’s ubiquitous language. As Eric Evans put it, the code must be based on the same language the developers use when they speak with one another and with domain experts. This is especially important for implementing complex business logic.
Now let’s take a look at the third and final building block of a domain model.
迟早,您可能会遇到业务逻辑不属于任何聚合或值对象,或者似乎与多个聚合相关。在这种情况下,领域驱动设计建议将逻辑实现为领域服务。
Sooner or later, you may encounter business logic that either doesn’t belong to any aggregate or value object, or that seems to be relevant to multiple aggregates. In such cases, domain-driven design proposes to implement the logic as a domain service.
域服务是实现业务逻辑的无状态对象。在绝大多数情况下,此类逻辑会协调对系统各个组件的调用以执行某些计算或分析。
A domain service is a stateless object that implements the business logic. In the vast majority of cases, such logic orchestrates calls to various components of the system to perform some calculation or analysis.
让我们回到工单聚合的例子。回想一下,指定的代理向客户提出解决方案的时间有限。时间范围不仅取决于工单的数据(其优先级和升级状态),还取决于代理部门关于每个优先级的 SLA 的政策和代理的工作时间表(轮班)——我们不能期望代理在期间做出响应下班时间。
Let’s go back to the example of the ticket aggregate. Recall that the assigned agent has a limited time frame in which to propose a solution to the customer. The time frame depends not only on the ticket’s data (its priority and escalation status), but also on the agent’s department policy regarding the SLAs for each priority and the agent’s work schedule (shifts)—we can’t expect the agent to respond during off-hours.
响应时间范围计算逻辑需要来自多个来源的信息:工单、指定代理的部门和工作时间表。这使得它成为作为域服务实现的理想候选者:
The response time frame calculation logic requires information from multiple sources: the ticket, the assigned agent’s department, and the work schedule. That makes it an ideal candidate to be implemented as a domain service:
publicclassResponseTimeFrameCalculationService{...publicResponseTimeframeCalculateAgentResponseDeadline(UserIdagentId,Prioritypriority,boolescalated,DateTimestartTime){varpolicy=_departmentRepository.GetDepartmentPolicy(agentId);varmaxProcTime=policy.GetMaxResponseTimeFor(priority);if(escalated){maxProcTime=maxProcTime*policy.EscalationFactor;}varshifts=_departmentRepository.GetUpcomingShifts(agentId,startTime,startTime.Add(policy.MaxAgentResponseTime));returnCalculateTargetTime(maxProcTime,shifts);}...}
publicclassResponseTimeFrameCalculationService{...publicResponseTimeframeCalculateAgentResponseDeadline(UserIdagentId,Prioritypriority,boolescalated,DateTimestartTime){varpolicy=_departmentRepository.GetDepartmentPolicy(agentId);varmaxProcTime=policy.GetMaxResponseTimeFor(priority);if(escalated){maxProcTime=maxProcTime*policy.EscalationFactor;}varshifts=_departmentRepository.GetUpcomingShifts(agentId,startTime,startTime.Add(policy.MaxAgentResponseTime));returnCalculateTargetTime(maxProcTime,shifts);}...}
领域服务使协调多个聚合的工作变得容易。但是,始终牢记聚合模式的局限性很重要,即在一个数据库事务中只能修改聚合的一个实例。领域服务并不是绕过这个限制的漏洞。每笔交易一个实例的规则仍然适用。相反,域服务有助于实现需要读取多个聚合数据的计算逻辑。
Domain services make it easy to coordinate the work of multiple aggregates. However, it is important to always keep in mind the aggregate pattern’s limitation of modifying only one instance of an aggregate in one database transaction. Domain services are not a loophole around this limitation. The rule of one instance per transaction still holds true. Instead, domain services lend themselves to implementing calculation logic that requires reading the data of multiple aggregates.
同样重要的是要指出,域服务与微服务、面向服务的体系结构或软件工程中服务一词的几乎任何其他用法都无关。它只是一个用于承载业务逻辑的无状态对象。
It is also important to point out that domain services have nothing to do with microservices, service-oriented architecture, or almost any other use of the word service in software engineering. It is just a stateless object used to host business logic.
正如本章介绍中所指出的,聚合和值对象模式是作为解决问题的一种手段引入的。业务逻辑实现的复杂性。让我们看看这背后的原因。
As noted in this chapter’s introduction, the aggregate and value object patterns were introduced as a means for tackling complexity in the implementation of business logic. Let’s see the reasoning behind this.
企业管理大师 Eliyahu M. Goldratt在他的《选择》一书中概述了系统复杂性的简洁而有力的定义。根据 Goldratt 的说法,在讨论系统的复杂性时,我们感兴趣的是评估控制和预测系统行为的难度。这两个方面都反映在系统的自由度上。
In his book The Choice, business management guru Eliyahu M. Goldratt outlines a succinct yet powerful definition of system complexity. According to Goldratt, when discussing the complexity of a system we are interested in evaluating the difficulty of controlling and predicting the system’s behavior. These two aspects are reflected by the system’s degrees of freedom.
系统的自由度是描述其状态所需的数据点。考虑以下两个类:
A system’s degrees of freedom are the data points needed to describe its state. Consider the following two classes:
publicclassClassA{publicintA{get;set;}publicintB{get;set;}publicintC{get;set;}publicintD{get;set;}publicintE{get;set;}}publicclassClassB{privateint_a,_d;publicintA{get=>_a;set{_a=value;B=value/2;C=value/3;}}publicintB{get;privateset;}publicintC{get;privateset;}publicintD{get=>_d;set{_d=value;E=value*2}}publicintE{get;privateset;}}
publicclassClassA{publicintA{get;set;}publicintB{get;set;}publicintC{get;set;}publicintD{get;set;}publicintE{get;set;}}publicclassClassB{privateint_a,_d;publicintA{get=>_a;set{_a=value;B=value/2;C=value/3;}}publicintB{get;privateset;}publicintC{get;privateset;}publicintD{get=>_d;set{_d=value;E=value*2}}publicintE{get;privateset;}}
乍一看,似乎ClassB比.复杂得多ClassA。它具有相同数量的变量,但除此之外,它还实现了额外的计算。它比 更复杂吗ClassA?
At first glance, it seems that ClassB is much more complex than ClassA. It has the same number of variables, but on top of that, it implements additional calculations. Is it more complex than ClassA?
让我们从自由度的角度分析这两个类。您需要多少数据元素来描述 的状态ClassA?答案是五个:它的五个变量。因此,ClassA有五个自由度。
Let’s analyze both classes from the degrees-of-freedom perspective. How many data elements do you need to describe the state of ClassA? The answer is five: its five variables. Hence, ClassA has five degrees of freedom.
您需要多少数据元素来描述 的状态ClassB?如果您查看属性A和的分配逻辑,您会注意到、和D的值是和的值的函数。如果您知道什么是什么,那么您可以推断出其余变量的值。因此,只有两个自由度。你只需要两个值来描述它的状态。BCEADADClassB
How many data elements do you need to describe the state of ClassB? If you look at the assignment logic for properties A and D, you will notice that the values of B, C, and E are functions of the values of A and D. If you know what A and D are, then you can deduce the values of the rest of the variables. Therefore, ClassB has only two degrees of freedom. You need only two values to describe its state.
回到最初的问题,哪个类在控制和预测其行为方面更困难?答案是自由度更高的那个,或者ClassA。引入的不变量ClassB降低了它的复杂性。这就是聚合和值对象模式所做的:封装不变量,从而降低复杂性。
Going back to the original question, which class is more difficult in terms of controlling and predicting its behavior? The answer is the one with more degrees of freedom, or ClassA. The invariants introduced in ClassB reduce its complexity. That’s what both aggregate and value object patterns do: encapsulate invariants and thus reduce complexity.
所有与值对象状态相关的业务逻辑都位于其边界内。聚合也是如此。聚合只能通过其自身的方法进行修改。它的业务逻辑对业务不变量进行了封装和保护,从而降低了自由度。
All the business logic related to the state of a value object is located in its boundaries. The same is true for aggregates. An aggregate can only be modified by its own methods. Its business logic encapsulates and protects business invariants, thus reducing the degrees of freedom.
由于域模型模式仅适用于具有复杂业务逻辑的子域,因此可以安全地假设这些是核心子域——软件的核心。
Since the domain model pattern is applied only for subdomains with complex business logic, it’s safe to assume that these are core subdomains—the heart of the software.
领域模型模式针对复杂业务逻辑的情况。它由三个主要构建块组成:
The domain model pattern is aimed at cases of complex business logic. It consists of three main building blocks:
业务领域的概念可以通过它们的值唯一标识,因此不需要显式 ID 字段。由于其中一个字段的更改在语义上会创建一个新值,因此值对象是不可变的。
值对象不仅对数据建模,而且对行为建模:方法操纵值并因此初始化新的值对象。
Concepts of the business domain that can be identified exclusively by their values and thus do not require an explicit ID field. Since a change in one of the fields semantically creates a new value, value objects are immutable.
Value objects model not only data, but behavior as well: methods manipulating the values and thus initializing new value objects.
共享事务边界的实体层次结构。聚合边界中包含的所有数据必须高度一致才能实现其业务逻辑。
聚合的状态及其内部对象只能通过其公共接口,通过执行聚合的命令来修改。数据字段对于外部组件是只读的,以确保与聚合相关的所有业务逻辑都位于其边界内。
聚合充当事务边界。它的所有数据,包括所有内部对象,都必须作为一个原子事务提交给数据库。
聚合可以通过发布域事件与外部实体进行通信,域事件是描述聚合生命周期中重要业务事件的消息。其他组件可以订阅事件并使用它们来触发业务逻辑的执行。
A hierarchy of entities sharing a transactional boundary. All of the data included in an aggregate’s boundary has to be strongly consistent to implement its business logic.
The state of the aggregate, and its internal objects, can only be modified through its public interface, by executing the aggregate’s commands. The data fields are read-only for external components for the sake of ensuring that all the business logic related to the aggregate resides in its boundaries.
The aggregate acts as a transactional boundary. All of its data, including all of its internal objects, has to be committed to the database as one atomic transaction.
An aggregate can communicate with external entities by publishing domain events—messages describing important business events in the aggregate’s lifecycle. Other components can subscribe to the events and use them to trigger the execution of business logic.
领域模型的构建块通过将业务逻辑封装在值对象和聚合的边界中来解决业务逻辑的复杂性。无法从外部修改对象的状态,确保所有相关业务逻辑都在聚合和值对象的边界实现,不会在应用层重复。
The domain model’s building blocks tackle the complexity of the business logic by encapsulating it in the boundaries of value objects and aggregates. The inability to modify the objects’ state externally ensures that all the relevant business logic is implemented in the boundaries of aggregates and value objects and won’t be duplicated in the application layer.
在下一章中,您将学习实现域模型模式的高级方法,这一次使时间维度成为模型的固有部分。
In the next chapter, you will learn the advanced way to implement the domain model pattern, this time making the dimension of time an inherent part of the model.
下列哪项为真?
值对象只能包含数据。
值对象只能包含行为。
值对象是不可变的。
值对象的状态可以改变。
Which of the following statements is true?
Value objects can only contain data.
Value objects can only contain behavior.
Value objects are immutable.
Value objects’ state can change.
设计聚合边界的一般指导原则是什么?
聚合只能包含一个实体,因为在单个数据库事务中只能包含聚合的一个实例。
只要业务领域的数据一致性要求完好无损,聚合就应该设计得尽可能小。
聚合表示实体的层次结构。因此,为了最大限度地提高系统数据的一致性,应该将聚合设计得尽可能宽。
这取决于:对于某些业务领域,小聚合是最好的,而在其他领域,使用尽可能大的聚合会更有效。
What is the general guiding principle for designing the boundary of an aggregate?
An aggregate can contain only one entity as only one instance of an aggregate can be included in a single database transaction.
Aggregates should be designed to be as small as possible, as long as the business domain’s data consistency requirements are intact.
An aggregate represents a hierarchy of entities. Therefore, to maximize the consistency of the system’s data, aggregates should be designed to be as wide as possible.
It depends: for some business domains small aggregates are best, while in others it’s more efficient to work with aggregates that are as large as possible.
为什么在一个事务中只能提交聚合的一个实例?
以确保模型能够在高负载下执行。
确保正确的事务边界。
没有这样的要求;这取决于业务领域。
使使用不支持多记录事务的数据库成为可能,例如键值和文档存储。
Why can only one instance of an aggregate be committed in one transaction?
To ensure that the model can perform under high load.
To ensure correct transactional boundaries.
There is no such requirement; it depends on the business domain.
To make it possible to work with databases that do not support multirecord transactions, such as key–value and document stores.
以下哪项陈述最能描述域模型构建块之间的关系?
值对象描述实体的属性。
值对象可以发出领域事件。
聚合包含一个或多个实体。
A和C。
Which of the following statements best describes the relationships between the building blocks of a domain model?
Value objects describe entities’ properties.
Value objects can emit domain events.
An aggregate contains one or more entities.
A and C.
以下哪项关于活动记录和聚合之间差异的陈述是正确的?
活动记录仅包含数据,而聚合还包含行为。
聚合封装了它的所有业务逻辑,但操作活动记录的业务逻辑可以位于其边界之外。
聚合只包含数据,而活动记录包含数据和行为。
聚合包含一组活动记录。
Which of the following statements is correct about differences between active records and aggregates?
Active records contain only data, whereas aggregates also contain behavior.
An aggregate encapsulates all of its business logic, but business logic manipulating an active record can be located outside of its boundary.
Aggregates contain only data, whereas active records contain both data and behavior.
An aggregate contains a set of active records.
1个福勒,M. (2002)。企业应用架构模式。波士顿:Addison-Wesley。
1 Fowler, M. (2002). Patterns of Enterprise Application Architecture. Boston: Addison-Wesley.
2个本章中的所有代码示例都将使用面向对象的编程语言。然而,所讨论的概念并不局限于 OOP,并且与函数式编程范式相关。
2 All the code samples in this chapter will use an object-oriented programming language. However, the discussed concepts are not limited to OOP and are as relevant for the functional programming paradigm.
3个.NET 中的 POCO、Java 中的 POJO、Python 中的 POPO 等。
3 POCOs in .NET, POJOs in Java, POPOs in Python, etc.
4个“原始的痴迷。” (nd) 于 2021 年 6 月 13 日从https://wiki.c2.com/?PrimitiveObsession检索。
4 “Primitive Obsession.” (n.d.) Retrieved June 13, 2021, from https://wiki.c2.com/?PrimitiveObsession.
5个在 C# 9.0 中,新类型record实现了基于值的相等性,因此不需要重写相等性运算符。
5 In C# 9.0, the new type record implements value-based equality and thus doesn’t require overriding the equality operators.
6个还称为服务层,是将公共 API 操作转发到域模型的系统部分。
6 Also known as a service layer, the part of the system that forwards public API actions to the domain model.
7本质上,应用层的操作实现了事务脚本模式。它必须将操作编排为原子事务。对整个聚合的更改要么成功要么失败,但永远不会提交部分更新的状态。
7 In essence, the application layer’s operations implement the transaction script pattern. It has to orchestrate the operation as an atomic transaction. The changes to the whole aggregate either succeed or fail, but never commit a partially updated state.
8个回想一下,应用层是事务脚本的集合,正如我们在第 5 章中讨论的那样,并发管理对于防止竞争更新破坏系统数据至关重要。
8 Recall that the application layer is a collection of transaction scripts, and as we discussed in Chapter 5, concurrency management is essential to prevent competing updates from corrupting the system’s data.
在上一章中,您了解了领域模型模式:它的构建块,目的和应用上下文。事件源领域模型模式基于与领域模型模式相同的前提。同样,业务逻辑复杂,属于核心子域。此外,它使用与域模型相同的战术模式:值对象、聚合和域事件。
In the previous chapter, you learned about the domain model pattern: its building blocks, purpose, and application context. The event-sourced domain model pattern is based on the same premise as the domain model pattern. Again, the business logic is complex and belongs to a core subdomain. Moreover, it uses the same tactical patterns as the domain model: value objects, aggregates, and domain events.
这些实现模式的区别在于方式聚合的状态是持久的。事件源域模型使用事件源模式来管理聚合的状态:模型生成描述每个更改的域事件,并将它们用作聚合数据的真实来源,而不是持久化聚合的状态。
The difference between these implementation patterns lies in the way the aggregates’ state is persisted. The event-sourced domain model uses the event sourcing pattern to manage the aggregates’ states: instead of persisting an aggregate’s state, the model generates domain events describing each change and uses them as the source of truth for the aggregate’s data.
本章首先介绍事件溯源的概念。然后介绍了如何将事件溯源与领域模型模式相结合,使其成为事件溯源领域模型。
This chapter starts by introducing the notion of event sourcing. Then it covers how event sourcing can be combined with the domain model pattern, making it an event-sourced domain model.
给我看你的流程图,隐藏你的表格,我将继续感到困惑。给我看你的表格,我通常不需要你的流程图;这将是显而易见的。
弗雷德·布鲁克斯一
Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowchart; it’ll be obvious.
Fred Brooks1
让我们使用 Fred Brooks 的推理来定义事件溯源模式,并了解它与传统建模和数据持久化有何不同。检查表 7-1并分析您可以从该数据中了解到有关它所属系统的哪些信息。
Let’s use Fred Brooks’s reasoning to define the event sourcing pattern and understand how it differs from traditional modeling and persisting of data. Examine Table 7-1 and analyze what you can learn from this data about the system it belongs to.
| 线索编号 | 名 | 姓 | 地位 | 电话号码 | 跟进 | 创建于 | 更新时间 |
|---|---|---|---|---|---|---|---|
| 1个 | 肖恩 | 卡拉汉 | 转换 | 555-1246 | 2019- 01-31 T 10:02:40.32Z | 2019- 01-31 T 10:02:40.32Z | |
| 2个 | 莎拉 | 埃斯特拉达 | 关闭 | 555-4395 | 2019- 03-29 T 22:01:41.44Z | 2019- 03-29 T 22:01:41.44Z | |
| 3个 | 斯蒂芬妮 | 棕色的 | 关闭 | 555-1176 | 2019- 04-15 T 23:08:45.59Z | 2019- 04-15 T 23:08:45.59Z | |
| 4个 | 萨米 | 卡尔霍恩 | 关闭 | 555-1850 | 2019- 04-25 T 05:42:17.07Z | 2019- 04-25 T 05:42:17.07Z | |
| 5个 | 威廉 | 史密斯 | 转换 | 555-3013 | 2019- 05-14 T 04:43:57.51Z | 2019- 05-14 T 04:43:57.51Z | |
| 6个 | 萨布里 | 陈 | 新领导 | 555-2900 | 2019- 06-19 T 15:01:49.68Z | 2019- 06-19 T 15:01:49.68Z | |
| 7 | 萨曼莎 | 埃斯皮诺萨 | 新领导 | 555-8861 | 2019- 07-17 T 13:09:59.32Z | 2019- 07-17 T 13:09:59.32Z | |
| 8个 | 哈尼语 | 克罗宁 | 关闭 | 555-3018 | 2019- 10-09 T 11:40:17.13Z | 2019- 10-09 T 11:40:17.13Z | |
| 9 | 西安 | 埃斯皮诺萨 | FOLLOWUP_SET | 555-6461 | 2019- 12-04 T 01:49:08.05Z | 2019- 12-04 T 01:49:08.05Z | 2019- 12-04 T 01:49:08.05Z |
| 10 | 索菲亚 | 埃斯卡米拉 | 关闭 | 555-4090 | 2019- 12-06 T 09:12:32.56Z | 2019- 12-06 T 09:12:32.56Z | |
| 11 | 威廉 | 白色的 | FOLLOWUP_SET | 555-1187 | 2020- 01-23 T 00:33:13.88Z | 2020- 01-23 T 00:33:13.88Z | 2020- 01-23 T 00:33:13.88Z |
| 12 | 凯西 | 戴维斯 | 转换 | 555-8101 | 2020- 05-20 T 09:52:55.95Z | 2020- 05-27 T 12:38:44.12Z | |
| 13 | 沃尔特 | 康纳 | 新领导 | 555-4753 | 2020- 04-20 T 06:52:55.95Z | 2020- 04-20 T 06:52:55.95Z | |
| 14 | 苏菲 | 加西亚 | 转换 | 555-1284 | 2020- 05-06 T 18:47:04.70Z | 2020- 05-06 T 18:47:04.70Z | |
| 15 | 莎莉 | 埃文斯 | 支付失败 | 555-3230 | 2020- 06-04 T 14:51:06.15Z | 2020- 06-04 T 14:51:06.15Z | |
| 16 | 斯科特 | 查特曼 | 新领导 | 555-6953 | 2020- 06-09 T 09:07:05.23Z | 2020- 06-09 T 09:07:05.23Z | |
| 17 | 斯蒂芬 | 平克曼 | 转换 | 555-2326 | 2020- 07-20 T 00:56:59.94Z | 2020- 07-20 T 00:56:59.94Z | |
| 18 | 萨拉 | 埃利奥特 | 待付款 | 555-2620 | 2020- 08-12 T 17:39:43.25Z | 2020- 08-12 T 17:39:43.25Z | |
| 19 | 赛迪 | 爱德华兹 | FOLLOWUP_SET | 555-8163 | 2020- 10-22 T 12:40:03.98Z | 2020- 10-22 T 12:40:03.98Z | 2020- 10-22 T 12:40:03.98Z |
| 20 | 威廉 | 史密斯 | 待付款 | 555-9273 | 2020- 11-13 T 08:14:07.17Z | 2020- 11-13 T 08:14:07.17Z |
很明显,该表用于管理电话营销系统中的潜在客户或潜在客户。对于每个潜在客户,您可以看到他们的 ID、他们的名字和姓氏、创建和更新记录的时间、他们的电话号码以及潜在客户的当前状态。
It’s evident that the table is used to manage potential customers, or leads, in a telemarketing system. For each lead, you can see their ID, their first and last names, when the record was created and updated, their phone number, and the lead’s current status.
通过检查各种状态,我们还可以假设每个潜在客户经历的处理周期:
By examining the various statuses, we can also assume the processing cycle each potential customer goes through:
销售流程从NEW_LEAD状态中的潜在客户开始。
The sales flow starts with the potential customer in the NEW_LEAD status.
销售电话可以以对报价不感兴趣(潜在客户是CLOSED)、安排跟进电话 ( FOLLOWUP_SET) 或接受报价 ( PENDING_PAYMENT) 结束。
A sales call can end with the person not being interested in the offer (the lead is CLOSED), scheduling a follow-up call (FOLLOWUP_SET), or accepting the offer (PENDING_PAYMENT).
如果付款成功,则潜在CONVERTED客户成为客户。相反,付款可能会失败PAYMENT_FAILED—— 。
If the payment is successful, the lead is CONVERTED into a customer. Conversely, the payment can fail—PAYMENT_FAILED.
我们仅通过分析表的模式和存储在其中的数据就可以收集到大量信息。我们甚至可以假设在对数据建模时使用了哪种通用语言。但是该表中缺少什么信息?
That’s quite a lot of information that we can gather just by analyzing a table’s schema and the data stored in it. We can even assume what ubiquitous language was used when modeling the data. But what information is missing from that table?
该表的数据记录了潜在客户的当前状态,但它遗漏了每个潜在客户如何达到当前状态的故事。我们无法分析潜在客户生命周期中发生的事情。我们不知道在线索变成之前打了多少个电话CONVERTED。是立即购买,还是经过漫长的销售过程?根据历史数据,是值得在多次跟进后尝试联系一个人,还是关闭潜在客户并转向更有希望的潜在客户更有效?这些信息都没有。我们所知道的只是线索的当前状态。
The table’s data documents the leads’ current states, but it misses the story of how each lead got to their current state. We can’t analyze what was happening during the lifecycles of leads. We don’t know how many calls were made before a lead became CONVERTED. Was a purchase made right away, or was there a lengthy sales journey? Based on the historical data, is it worth trying to contact a person after multiple follow-ups, or is it more efficient to close the lead and move to a more promising prospect? None of that information is there. All we know are the leads’ current states.
这些问题反映了对优化销售流程至关重要的业务关注点。从业务的角度来看,分析数据并根据经验优化流程至关重要。填补缺失信息的方法之一是使用事件溯源。
These questions reflect business concerns essential for optimizing the sales process. From a business standpoint, it’s crucial to analyze the data and optimize the process based on the experience. One of the ways to fill in the missing information is to use event sourcing.
事件溯源模式将时间维度引入数据模型。基于事件溯源的系统不是反映聚合当前状态的模式,而是持久保存记录聚合生命周期中每个更改的事件。
The event sourcing pattern introduces the dimension of time into the data model. Instead of the schema reflecting the aggregates’ current state, an event sourcing–based system persists events documenting every change in an aggregate’s lifecycle.
考虑表 7-1CONVERTED中第 12 行的客户。以下清单演示了该人的数据将如何在事件源系统中表示:
Consider the CONVERTED customer on line 12 in Table 7-1. The following listing demonstrates how the person’s data would be represented in an event-sourced system:
{"lead-id":12,"event-id":0,"event-type":"lead-initialized","first-name":"Casey","last-name":"David","phone-number":"555-2951","timestamp":"2020-05-20T09:52:55.95Z"},{"lead-id":12,"event-id":1,"event-type":"contacted","timestamp":"2020-05-20T12:32:08.24Z"},{"lead-id":12,"event-id":2,"event-type":"followup-set","followup-on":"2020-05-27T12:00:00.00Z","timestamp":"2020-05-20T12:32:08.24Z"},{"lead-id":12,"event-id":3,"event-type":"contact-details-updated","first-name":"Casey","last-name":"Davis","phone-number":"555-8101","timestamp":"2020-05-20T12:32:08.24Z"},{"lead-id":12,"event-id":4,"event-type":"contacted","timestamp":"2020-05-27T12:02:12.51Z"},{"lead-id":12,"event-id":5,"event-type":"order-submitted","payment-deadline":"2020-05-30T12:02:12.51Z","timestamp":"2020-05-27T12:02:12.51Z"},{"lead-id":12,"event-id":6,"event-type":"payment-confirmed","status":"converted","timestamp":"2020-05-27T12:38:44.12Z"}
{"lead-id":12,"event-id":0,"event-type":"lead-initialized","first-name":"Casey","last-name":"David","phone-number":"555-2951","timestamp":"2020-05-20T09:52:55.95Z"},{"lead-id":12,"event-id":1,"event-type":"contacted","timestamp":"2020-05-20T12:32:08.24Z"},{"lead-id":12,"event-id":2,"event-type":"followup-set","followup-on":"2020-05-27T12:00:00.00Z","timestamp":"2020-05-20T12:32:08.24Z"},{"lead-id":12,"event-id":3,"event-type":"contact-details-updated","first-name":"Casey","last-name":"Davis","phone-number":"555-8101","timestamp":"2020-05-20T12:32:08.24Z"},{"lead-id":12,"event-id":4,"event-type":"contacted","timestamp":"2020-05-27T12:02:12.51Z"},{"lead-id":12,"event-id":5,"event-type":"order-submitted","payment-deadline":"2020-05-30T12:02:12.51Z","timestamp":"2020-05-27T12:02:12.51Z"},{"lead-id":12,"event-id":6,"event-type":"payment-confirmed","status":"converted","timestamp":"2020-05-27T12:38:44.12Z"}
列表中的事件讲述了客户的故事。潜在客户在系统中创建(事件 0),大约两小时后由销售代理联系(事件 1)。在通话过程中,商定销售代理将在一周后回电(事件 2),但会拨打不同的电话号码(事件 3)。销售代理还修复了姓氏中的拼写错误(事件 3)。在商定的日期和时间(事件 4)联系了潜在客户并提交了订单(事件 5)。订单本应在三天内付款(事件 5),但大约半小时后收到付款(事件 6),潜在客户转化为新客户。
The events in the listing tell the customer’s story. The lead was created in the system (event 0) and was contacted by a sales agent about two hours later (event 1). During the call, it was agreed that the sales agent would call back a week later (event 2), but to a different phone number (event 3). The sales agent also fixed a typo in the last name (event 3). The lead was contacted on the agreed date and time (event 4) and submitted an order (event 5). The order was to be paid in three days (event 5), but the payment was received about half an hour later (event 6), and the lead was converted into a new customer.
正如我们之前看到的,客户的状态可以很容易地从这些领域事件中投射出来。我们所要做的就是对每个事件顺序应用简单的转换逻辑:
As we saw earlier, the customer’s state can easily be projected out from these domain events. All we have to do is apply simple transformation logic sequentially to each event:
publicclassLeadSearchModelProjection{publiclongLeadId{get;privateset;}publicHashSet<string>FirstNames{get;privateset;}publicHashSet<string>LastNames{get;privateset;}publicHashSet<PhoneNumber>PhoneNumbers{get;privateset;}publicintVersion{get;privateset;}publicvoidApply(LeadInitialized@event){LeadId=@event.LeadId;FirstNames=newHashSet<string>();LastNames=newHashSet<string>();PhoneNumbers=newHashSet<PhoneNumber>();FirstNames.Add(@event.FirstName);LastNames.Add(@event.LastName);PhoneNumbers.Add(@event.PhoneNumber);Version=0;}publicvoidApply(ContactDetailsChanged@event){FirstNames.Add(@event.FirstName);LastNames.Add(@event.LastName);PhoneNumbers.Add(@event.PhoneNumber);Version+=1;}publicvoidApply(Contacted@event){Version+=1;}publicvoidApply(FollowupSet@event){Version+=1;}publicvoidApply(OrderSubmitted@event){Version+=1;}publicvoidApply(PaymentConfirmed@event){Version+=1;}}
publicclassLeadSearchModelProjection{publiclongLeadId{get;privateset;}publicHashSet<string>FirstNames{get;privateset;}publicHashSet<string>LastNames{get;privateset;}publicHashSet<PhoneNumber>PhoneNumbers{get;privateset;}publicintVersion{get;privateset;}publicvoidApply(LeadInitialized@event){LeadId=@event.LeadId;FirstNames=newHashSet<string>();LastNames=newHashSet<string>();PhoneNumbers=newHashSet<PhoneNumber>();FirstNames.Add(@event.FirstName);LastNames.Add(@event.LastName);PhoneNumbers.Add(@event.PhoneNumber);Version=0;}publicvoidApply(ContactDetailsChanged@event){FirstNames.Add(@event.FirstName);LastNames.Add(@event.LastName);PhoneNumbers.Add(@event.PhoneNumber);Version+=1;}publicvoidApply(Contacted@event){Version+=1;}publicvoidApply(FollowupSet@event){Version+=1;}publicvoidApply(OrderSubmitted@event){Version+=1;}publicvoidApply(PaymentConfirmed@event){Version+=1;}}
迭代聚合的事件并将它们按顺序提供给方法的适当重写将精确地生成在表 7-1Apply中的表中建模的状态表示。
Iterating an aggregate’s events and feeding them sequentially into the appropriate overrides of the Apply method will produce precisely the state representation modeled in the table in Table 7-1.
关注Version领域在应用每个事件后递增。它的值表示对业务实体所做的修改总数。此外,假设我们应用事件的子集。在那种情况下,我们可以“穿越时空”:我们可以通过仅应用相关事件来投射实体在其生命周期的任何时刻的状态。例如,如果我们需要实体在版本 5 中的状态,我们可以只应用前五个事件。
Pay attention to the Version field that is incremented after applying each event. Its value represents the total number of modifications made to the business entity. Moreover, suppose we apply a subset of events. In that case, we can “travel through time”: we can project the entity’s state at any point of its lifecycle by applying only the relevant events. For example, if we need the entity’s state in version 5, we can apply only the first five events.
最后,我们不仅限于投影事件的单一状态表示!考虑以下场景。
Finally, we are not limited to projecting only a single state representation of the events! Consider the following scenarios.
您必须执行搜索。但是,由于潜在客户的联系信息可以更新——名字、姓氏和电话号码——销售代理可能不知道其他代理应用的更改,并且可能希望使用他们的联系信息(包括历史值)来定位潜在客户。我们可以很容易地投影出历史信息:
You have to implement a search. However, since a lead’s contact information can be updated—first name, last name, and phone number—sales agents may not be aware of the changes applied by other agents and may want to locate leads using their contact information, including historical values. We can easily project the historical information:
publicclassLeadSearchModelProjection{publiclongLeadId{get;privateset;}publicHashSet<string>FirstNames{get;privateset;}publicHashSet<string>LastNames{get;privateset;}publicHashSet<PhoneNumber>PhoneNumbers{get;privateset;}publicintVersion{get;privateset;}publicvoidApply(LeadInitialized@event){LeadId=@event.LeadId;FirstNames=newHashSet<string>();LastNames=newHashSet<string>();PhoneNumbers=newHashSet<PhoneNumber>();FirstNames.Add(@event.FirstName);LastNames.Add(@event.LastName);PhoneNumbers.Add(@event.PhoneNumber);Version=0;}publicvoidApply(ContactDetailsChanged@event){FirstNames.Add(@event.FirstName);LastNames.Add(@event.LastName);PhoneNumbers.Add(@event.PhoneNumber);Version+=1;}publicvoidApply(Contacted@event){Version+=1;}publicvoidApply(FollowupSet@event){Version+=1;}publicvoidApply(OrderSubmitted@event){Version+=1;}publicvoidApply(PaymentConfirmed@event){Version+=1;}}
publicclassLeadSearchModelProjection{publiclongLeadId{get;privateset;}publicHashSet<string>FirstNames{get;privateset;}publicHashSet<string>LastNames{get;privateset;}publicHashSet<PhoneNumber>PhoneNumbers{get;privateset;}publicintVersion{get;privateset;}publicvoidApply(LeadInitialized@event){LeadId=@event.LeadId;FirstNames=newHashSet<string>();LastNames=newHashSet<string>();PhoneNumbers=newHashSet<PhoneNumber>();FirstNames.Add(@event.FirstName);LastNames.Add(@event.LastName);PhoneNumbers.Add(@event.PhoneNumber);Version=0;}publicvoidApply(ContactDetailsChanged@event){FirstNames.Add(@event.FirstName);LastNames.Add(@event.LastName);PhoneNumbers.Add(@event.PhoneNumber);Version+=1;}publicvoidApply(Contacted@event){Version+=1;}publicvoidApply(FollowupSet@event){Version+=1;}publicvoidApply(OrderSubmitted@event){Version+=1;}publicvoidApply(PaymentConfirmed@event){Version+=1;}}
投影逻辑使用LeadInitialized和ContactDetailsChanged事件来填充潜在客户个人详细信息的相应集合。其他事件将被忽略,因为它们不影响特定模型的状态。
The projection logic uses the LeadInitialized and ContactDetailsChanged events to populate the respective sets of the lead’s personal details. Other events are ignored since they do not affect the specific model’s state.
将此投影逻辑应用于前面示例中的 Casey Davis 事件将导致以下状态:
Applying this projection logic to Casey Davis’s events from the earlier example will result in the following state:
铅号:12 名字:['凯西'] 姓氏:['大卫','戴维斯'] 电话号码:['555-2951', '555-8101'] 版本:6
LeadId: 12 FirstNames: ['Casey'] LastNames: ['David', 'Davis'] PhoneNumbers: ['555-2951', '555-8101'] Version: 6
您的商业智能部门要求您提供更多潜在客户数据的分析友好表示。对于他们当前的研究,他们希望获得为不同的线索安排的跟进电话的数量。稍后他们将过滤已转换和已关闭的销售线索数据,并使用该模型优化销售流程。让我们投影他们要求的数据:
Your business intelligence department asks you to provide a more analysis-friendly representation of the leads data. For their current research, they want to get the number of follow-up calls scheduled for different leads. Later they will filter the converted and closed leads data and use the model to optimize the sales process. Let’s project the data they are asking for:
publicclassAnalysisModelProjection{publiclongLeadId{get;privateset;}publicintFollowups{get;privateset;}publicLeadStatusStatus{get;privateset;}publicintVersion{get;privateset;}publicvoidApply(LeadInitialized@event){LeadId=@event.LeadId;Followups=0;Status=LeadStatus.NEW_LEAD;Version=0;}publicvoidApply(Contacted@event){Version+=1;}publicvoidApply(FollowupSet@event){Status=LeadStatus.FOLLOWUP_SET;Followups+=1;Version+=1;}publicvoidApply(ContactDetailsChanged@event){Version+=1;}publicvoidApply(OrderSubmitted@event){Status=LeadStatus.PENDING_PAYMENT;Version+=1;}publicvoidApply(PaymentConfirmed@event){Status=LeadStatus.CONVERTED;Version+=1;}}
publicclassAnalysisModelProjection{publiclongLeadId{get;privateset;}publicintFollowups{get;privateset;}publicLeadStatusStatus{get;privateset;}publicintVersion{get;privateset;}publicvoidApply(LeadInitialized@event){LeadId=@event.LeadId;Followups=0;Status=LeadStatus.NEW_LEAD;Version=0;}publicvoidApply(Contacted@event){Version+=1;}publicvoidApply(FollowupSet@event){Status=LeadStatus.FOLLOWUP_SET;Followups+=1;Version+=1;}publicvoidApply(ContactDetailsChanged@event){Version+=1;}publicvoidApply(OrderSubmitted@event){Status=LeadStatus.PENDING_PAYMENT;Version+=1;}publicvoidApply(PaymentConfirmed@event){Status=LeadStatus.CONVERTED;Version+=1;}}
前面的逻辑维护了后续事件在潜在客户事件中出现的次数的计数器。如果我们将此投影应用于聚合事件的示例,它将生成以下状态:
The preceding logic maintains a counter of the number of times follow-up events appeared in the lead’s events. If we were to apply this projection to the example of the aggregate’s events, it would generate the following state:
铅号:12 跟进:1 状态:已转换 版本:6
LeadId: 12 Followups: 1 Status: Converted Version: 6
前面示例中实现的逻辑将搜索优化和分析优化模型投影到内存中。但是,要实际实现所需的功能,我们必须将投影模型保存在数据库中。在第 8 章中,您将了解一种允许我们这样做的模式:命令查询责任分离 (CQRS)。
The logic implemented in the preceding examples projects the search-optimized and analysis-optimized models in-memory. However, to actually implement the required functionality, we have to persist the projected models in a database. In Chapter 8, you will learn about a pattern that allows us to do that: command-query responsibility segregation (CQRS).
为了使事件源模式起作用,对对象状态的所有更改应该作为事件来表示和持久化。这些事件成为系统的真实来源(因此模式的名称)。这个过程如图 7-1所示。
For the event sourcing pattern to work, all changes to an object’s state should be represented and persisted as events. These events become the system’s source of truth (hence the name of the pattern). This process is shown in Figure 7-1.
存储系统事件的数据库是唯一的强一致性存储:系统的真实来源。用于持久化事件的数据库的公认名称是event store。
The database that stores the system’s events is the only strongly consistent storage: the system’s source of truth. The accepted name for the database that is used for persisting events is event store.
事件存储不应允许修改或删除事件2因为它是仅追加存储。为了支持事件溯源模式的实现,事件存储至少必须支持以下功能:获取属于特定业务实体的所有事件并附加事件。例如:
The event store should not allow modifying or deleting the events2 since it’s append-only storage. To support implementation of the event sourcing pattern, at a minimum the event store has to support the following functionality: fetch all events belonging to a specific business entity and append the events. For example:
interfaceIEventStore{IEnumerable<Event>Fetch(GuidinstanceId);voidAppend(GuidinstanceId,Event[]newEvents,intexpectedVersion);}
interfaceIEventStore{IEnumerable<Event>Fetch(GuidinstanceId);voidAppend(GuidinstanceId,Event[]newEvents,intexpectedVersion);}
expectedVersion方法中的参数是Append实现乐观并发管理所必需的:当您附加新事件时,您还指定了您的决策所基于的实体的版本。如果它是stale,即在预期版本之后添加了新事件,则事件存储应该引发并发异常。
The expectedVersion argument in the Append method is needed to implement optimistic concurrency management: when you append new events, you also specify the version of the entity on which you are basing your decisions. If it’s stale, that is, new events were added after the expected version, the event store should raise a concurrency exception.
在大多数系统中,需要额外的端点来实现 CQRS 模式,我们将在下一章讨论。
In most systems, additional endpoints are needed for implementing the CQRS pattern, as we will discuss in the next chapter.
本质上,事件溯源模式并不是什么新鲜事。金融业使用事件来表示账本中的变化。分类帐是记录交易的仅附加日志。当前状态(例如,账户余额)总是可以通过“投影”分类帐的记录来推断。
In essence, the event sourcing pattern is nothing new. The financial industry uses events to represent changes in a ledger. A ledger is an append-only log that documents transactions. A current state (e.g., account balance) can always be deduced by “projecting” the ledger’s records.
原始领域模型维护状态表示它的聚合并发出选择的领域事件。事件源域模型专门使用域事件来对聚合的生命周期进行建模。对聚合状态的所有更改都必须表示为域事件。
The original domain model maintains a state representation of its aggregates and emits select domain events. The event-sourced domain model uses domain events exclusively for modeling the aggregates’ lifecycles. All changes to an aggregate’s state have to be expressed as domain events.
事件源聚合上的每个操作都遵循以下脚本:
Each operation on an event-sourced aggregate follows this script:
加载聚合的域事件。
Load the aggregate’s domain events.
重构状态表示——将事件投影到可用于制定业务决策的状态表示中。
Reconstitute a state representation—project the events into a state representation that can be used to make business decisions.
执行聚合的命令来执行业务逻辑,从而产生新的领域事件。
Execute the aggregate’s command to execute the business logic, and consequently, produce new domain events.
将新的域事件提交到事件存储。
Commit the new domain events to the event store.
回到Ticket聚合的例子从第 6 章开始,让我们看看如何将其实现为事件源聚合。
Going back to the example of the Ticket aggregate from Chapter 6, let’s see how it would be implemented as an event-sourced aggregate.
应用程序服务遵循前面描述的脚本:它加载相关票证的事件,重新水化聚合实例,调用相关命令,并将更改持久保存回数据库:
The application service follows the script described earlier: it loads the relevant ticket’s events, rehydrates the aggregate instance, calls the relevant command, and persists changes back to the database:
01publicclassTicketAPI02{03privateITicketsRepository_ticketsRepository;04...0506publicvoidRequestEscalation(TicketIdid,EscalationReasonreason)07{08varevents=_ticketsRepository.LoadEvents(id);09varticket=newTicket(events);10varoriginalVersion=ticket.Version;11varcmd=newRequestEscalation(reason);12ticket.Execute(cmd);13_ticketsRepository.CommitChanges(ticket,originalVersion);14}1516...17}
01publicclassTicketAPI02{03privateITicketsRepository_ticketsRepository;04...0506publicvoidRequestEscalation(TicketIdid,EscalationReasonreason)07{08varevents=_ticketsRepository.LoadEvents(id);09varticket=newTicket(events);10varoriginalVersion=ticket.Version;11varcmd=newRequestEscalation(reason);12ticket.Execute(cmd);13_ticketsRepository.CommitChanges(ticket,originalVersion);14}1516...17}
构造函数中聚合Ticket的再水化逻辑(第 27 行到第 31 行)实例化状态投影仪类的实例,TicketState并依次AppendEvent为每个票证事件调用其方法:
The Ticket aggregate’s rehydration logic in the constructor (lines 27 through 31) instantiates an instance of the state projector class, TicketState, and sequentially calls its AppendEvent method for each of the ticket’s events:
18publicclassTicket19{20...21privateList<DomainEvent>_domainEvents=newList<DomainEvent>();22privateTicketState_state;23...2425publicTicket(IEnumerable<IDomainEvents>events)26{27_state=newTicketState();28foreach(vareinevents)29{30AppendEvent(e);31}32}
18publicclassTicket19{20...21privateList<DomainEvent>_domainEvents=newList<DomainEvent>();22privateTicketState_state;23...2425publicTicket(IEnumerable<IDomainEvents>events)26{27_state=newTicketState();28foreach(vareinevents)29{30AppendEvent(e);31}32}
将AppendEvent传入事件传递给TicketState投影逻辑,从而生成票证当前状态的内存表示:
The AppendEvent passes the incoming events to the TicketState projection logic, thus generating the in-memory representation of the ticket’s current state:
33privatevoidAppendEvent(IDomainEvent@event)34{35_domainEvents.Append(@event);36// Dynamically call the correct overload of the "Apply" method.37((dynamic)state).Apply((dynamic)@event);38}
33privatevoidAppendEvent(IDomainEvent@event)34{35_domainEvents.Append(@event);36// Dynamically call the correct overload of the "Apply" method.37((dynamic)state).Apply((dynamic)@event);38}
与我们在上一章中看到的实现相反,事件源聚合的RequestEscalation方法没有显式地将IsEscalated标志设置为true。相反,它实例化适当的事件并将其传递给方法AppendEvent(第 43 和 44 行):
Contrary to the implementation we saw in the previous chapter, the event-sourced aggregate’s RequestEscalation method doesn’t explicitly set the IsEscalated flag to true. Instead, it instantiates the appropriate event and passes it to the AppendEvent method (lines 43 and 44):
39publicvoidExecute(RequestEscalationcmd)40{41if(!_state.IsEscalated&&_state.RemainingTimePercentage<=0)42{43varescalatedEvent=newTicketEscalated(_id,cmd.Reason);44AppendEvent(escalatedEvent);45}46}4748...49}
39publicvoidExecute(RequestEscalationcmd)40{41if(!_state.IsEscalated&&_state.RemainingTimePercentage<=0)42{43varescalatedEvent=newTicketEscalated(_id,cmd.Reason);44AppendEvent(escalatedEvent);45}46}4748...49}
添加到聚合的事件集合中的所有事件都被传递到类中的状态投影逻辑TicketState,其中相关字段的值根据事件的数据发生变化:
All events added to the aggregate’s events collection are passed to the state projection logic in the TicketState class, where the relevant fields’ values are mutated according to the events’ data:
50publicclassTicketState51{52publicTicketIdId{get;privateset;}53publicintVersion{get;privateset;}54publicboolIsEscalated{get;privateset;}55...56publicvoidApply(TicketInitialized@event)57{58Id=@event.Id;59Version=0;60IsEscalated=false;61....62}6364publicvoidApply(TicketEscalated@event)65{66IsEscalated=true;67Version+=1;68}6970...71}
50publicclassTicketState51{52publicTicketIdId{get;privateset;}53publicintVersion{get;privateset;}54publicboolIsEscalated{get;privateset;}55...56publicvoidApply(TicketInitialized@event)57{58Id=@event.Id;59Version=0;60IsEscalated=false;61....62}6364publicvoidApply(TicketEscalated@event)65{66IsEscalated=true;67Version+=1;68}6970...71}
Now let’s look at some of the advantages of leveraging event sourcing when implementing complex business logic.
与更传统的模型相比,其中聚合的当前状态保存在数据库中,事件源域模型需要更多的努力来对聚合进行建模。然而,这种方法带来了显着的优势,使得该模式在许多情况下都值得考虑:
Compared to the more traditional model, in which the aggregates’ current states are persisted in a database, the event-sourced domain model requires more effort to model the aggregates. However, this approach brings significant advantages that make the pattern worth considering in many scenarios:
正如领域事件可用于重建聚合的当前状态一样,它们还可以用于恢复聚合的所有过去状态。换句话说,您始终可以重构聚合的所有过去状态。
这通常在分析系统行为、检查系统决策和优化业务逻辑时完成。
Just as the domain events can be used to reconstitute an aggregate’s current state, they can also be used to restore all past states of the aggregate. In other words, you can always reconstitute all the past states of an aggregate.
This is often done when analyzing the system’s behavior, inspecting the system’s decisions, and optimizing the business logic.
Another common use case for reconstituting past states is retroactive debugging: you can revert the aggregate to the exact state it was in when a bug was observed.
持久化的领域事件代表了一个强一致的聚合状态发生的所有事情的审计日志。法律要求某些业务领域实施此类审计日志,而事件溯源提供了开箱即用的功能。
该模型对于管理货币或货币交易的系统特别方便。它使我们能够轻松地跟踪系统的决策和账户之间的资金流动。
The persisted domain events represent a strongly consistent audit log of everything that has happened to the aggregates’ states. Laws oblige some business domains to implement such audit logs, and event sourcing provides this out of the box.
This model is especially convenient for systems managing money or monetary transactions. It allows us to easily trace the system’s decisions and the flow of funds between accounts.
经典的乐观并发模型引发异常当读取的数据在写入时变得陈旧(被另一个进程覆盖)。
使用事件溯源时,我们可以更深入地了解在读取现有事件和编写新事件之间到底发生了什么。您可以查询并发附加到事件存储的确切事件,并做出业务领域驱动的决定,以确定新事件是否与尝试的操作发生冲突,或者其他事件是否无关紧要并且可以安全地继续进行。
The classic optimistic concurrency model raises an exception when the read data becomes stale—overwritten by another process—while it is being written.
When using event sourcing, we can gain deeper insight into exactly what has happened between reading the existing events and writing the new ones. You can query the exact events that were concurrently appended to the event store and make a business domain–driven decision as to whether the new events collide with the attempted operation or the additional events are irrelevant and it’s safe to proceed.
到目前为止,事件源领域模型似乎是实现业务逻辑的最终模式,因此应尽可能经常使用。当然,这与让业务领域的需求驱动设计决策的原则相矛盾。那么,让我们讨论一下该模式带来的一些挑战:
So far it may seem that the event-sourced domain model is the ultimate pattern for implementing business logic and thus should be used as often as possible. Of course, that would contradict the principle of letting the business domain’s needs drive the design decisions. So, let’s discuss some of the challenges presented by the pattern:
如果手头的任务不能证明使用模式是合理的,而是可以通过更简单的设计来解决,那么所有这些挑战都会更加严峻。在第 10 章中,您将学习可以帮助您决定使用哪种业务逻辑实现模式的简单经验法则。
All of these challenges are even more acute if the task at hand doesn’t justify the use of the pattern and instead can be addressed by a simpler design. In Chapter 10, you will learn simple rules of thumb that can help you decide which business logic implementation pattern to use.
当向工程师介绍事件溯源模式时,他们经常会问几个常见问题,因此我觉得有必要在本章中解决这些问题。
When engineers are introduced to the event sourcing pattern, they often ask several common questions, so I find it obligatory to address them in this chapter.
将事件投影到状态表示中确实需要计算能力,并且随着更多事件被添加到聚合列表中,这种需求将会增长。
衡量投影对性能的影响很重要:处理成百上千个事件的效果。应将结果与聚合的预期寿命进行比较——预期在平均寿命期间记录的事件数。
在大多数系统中,只有在每次聚合超过 10,000 个事件后,性能影响才会明显。也就是说,在绝大多数系统中,聚合的平均生命周期不会超过 100 个事件。
在投影状态确实成为性能问题的极少数情况下,可以实现另一种模式:快照。如图 7-2所示,该模式实现了以下步骤:
一个过程不断地迭代事件存储中的新事件,生成相应的投影,并将它们存储在缓存中。
需要内存中的投影来对聚合执行操作。在这种情况下:
该过程从缓存中获取当前状态投影。
该过程从事件存储中获取快照版本之后的事件。
附加事件在内存中应用于快照。
值得重申的是,快照模式是一种必须证明其合理性的优化。如果系统中的聚合不会保留 10,000 多个事件,那么实现快照模式只是一个偶然的复杂性。但在继续实施快照模式之前,我建议您退后一步,仔细检查聚合的边界。
Projecting events into a state representation indeed requires compute power, and that need will grow as more events are added to an aggregate’s list.
It’s important to benchmark a projection’s impact on performance: the effect of working with hundreds or thousands of events. The results should be compared with the expected lifespan of an aggregate—the number of events expected to be recorded during an average lifespan.
In most systems, the performance hit will be noticeable only after 10,000+ events per aggregate. That said, in the vast majority of systems, an aggregate’s average lifespan won’t go over 100 events.
In the rare cases when projecting states does become a performance issue, another pattern can be implemented: snapshot. This pattern, shown in Figure 7-2, implements the following steps:
A process continuously iterates new events in the event store, generates corresponding projections, and stores them in a cache.
An in-memory projection is needed to execute an action on the aggregate. In this case:
The process fetches the current state projection from the cache.
The process fetches the events that came after the snapshot version from the event store.
The additional events are applied in-memory to the snapshot.
It’s worth reiterating that the snapshot pattern is an optimization that has to be justified. If the aggregates in your system won’t persist 10,000+ events, implementing the snapshot pattern is just an accidental complexity. But before you go ahead and implement the snapshot pattern, I recommend that you take a step back and double-check the aggregate’s boundaries.
事件源模型易于扩展。由于所有与聚合相关的操作都是在单个聚合的上下文中完成的,事件存储可以通过聚合 ID 进行分片:属于聚合实例的所有事件都应该驻留在单个分片中(见图 7-3 )。
The event-sourced model is easy to scale. Since all aggregate-related operations are done in the context of a single aggregate, the event store can be sharded by aggregate IDs: all events belonging to an instance of an aggregate should reside in a single shard (see Figure 7-3).
这种需求可以通过易忘记的有效负载模式来解决:所有敏感信息都以加密形式包含在事件中。加密密钥存储在外部键值存储中:密钥存储,其中密钥是特定聚合的 ID,值是加密密钥。当必须删除敏感数据时,加密密钥将从密钥存储中删除。因此,事件中包含的敏感信息将无法再访问。
This need can be addressed with the forgettable payload pattern: all sensitive information is included in the events in encrypted form. The encryption key is stored in an external key–value store: the key storage, where the key is a specific aggregate’s ID and the value is the encryption key. When the sensitive data has to be deleted, the encryption key is deleted from the key storage. As a result, the sensitive information contained in the events is no longer accessible.
将数据同时写入操作数据库和日志文件是一种容易出错的操作。本质上,它是针对两种存储机制的事务:数据库和文件。如果第一个失败,则必须回滚第二个。例如,如果数据库事务失败,没有人会关心删除之前的日志消息。因此,这样的日志不是一致的,而是最终不一致的。
Writing data both to an operational database and to a logfile is an error-prone operation. In its essence, it’s a transaction against two storage mechanisms: the database and the file. If the first one fails, the second one has to be rolled back. For example, if a database transaction fails, no one cares to delete the prior log messages. Hence, such logs are not consistent, but rather, eventually inconsistent.
从基础设施的角度来看,这种方法确实提供了状态和日志记录之间的一致同步。但是,它仍然容易出错。如果将来从事代码库工作的工程师忘记附加适当的日志记录怎么办?
此外,当基于状态的表示被用作真相的来源时,附加日志表的架构通常会很快退化为混乱。无法强制写入所有必需的信息并且以正确的格式写入。
From an infrastructural perspective, this approach does provide consistent synchronization between the state and the log records. However, it is still error prone. What if the engineer who will be working on the codebase in the future forgets to append an appropriate log record?
Furthermore, when the state-based representation is used as the source of truth, the additional log table’s schema usually degrades into chaos quickly. There is no way to enforce that all required information is written and that it is written in the correct format.
这种方法克服了前一种方法的缺点:不需要明确的手动调用来将记录追加到日志表中。也就是说,由此产生的历史记录只包括枯燥的事实:哪些领域被改变了。它遗漏了业务背景:为什么字段被更改。缺乏“为什么”极大地限制了投射额外模型的能力。
This approach overcomes the previous one’s drawback: no explicit manual calls are needed to append records to the log table. That said, the resultant history only includes the dry facts: what fields were changed. It misses the business contexts: why the fields were changed. The lack of “why” drastically limits the ability to project additional models.
本章解释了事件溯源模式及其在域模型聚合中对时间维度建模的应用。
This chapter explained the event sourcing pattern and its application for modeling the dimension of time in the domain model’s aggregates.
在事件源领域模型中,对聚合状态的所有更改都表示为一系列领域事件。这与状态更改仅更新数据库中的记录的更传统方法形成对比。生成的域事件可用于预测聚合的当前状态。此外,基于事件的模型使我们能够灵活地将事件投射到多个表示模型中,每个表示模型都针对特定任务进行了优化。
In an event-sourced domain model, all changes to an aggregate’s state are expressed as a series of domain events. That’s in contrast to the more traditional approaches in which a state change just updates a record in the databases. The resultant domain events can be used to project the aggregate’s current state. Moreover, the event-based model gives us the flexibility to project the events into multiple representation models, each optimized for a specific task.
这种模式适用于深入了解系统数据至关重要的情况,无论是为了分析和优化,还是因为法律要求审计日志。
This pattern fits cases in which it’s crucial to have deep insight into the system’s data, whether for analysis and optimization or because an audit log is required by law.
本章完成了我们对建模和实现业务逻辑的不同方法的探索。在下一章中,我们会将注意力转移到属于更高范围的模式:架构模式。
This chapter completes our exploration of the different ways to model and implement business logic. In the next chapter, we will shift our attention to patterns belonging to a higher scope: architectural patterns.
关于领域事件和值对象之间的关系,下列哪项陈述是正确的?
领域事件使用值对象来描述业务领域中发生的事情。
在实现事件源领域模型时,值对象应该重构为事件源聚合。
值对象与领域模型模式相关,并在事件源领域模型中被领域事件取代。
所有的陈述都是不正确的。
Which of the following statements is correct regarding the relationship between domain events and value objects?
Domain events use value objects to describe what has happened in the business domain.
When implementing an event-sourced domain model, value objects should be refactored into event-sourced aggregates.
Value objects are relevant for the domain model pattern, and are replaced by domain events in the event-sourced domain model.
All of the statements are incorrect.
关于从一系列事件中投射状态的选项,下列哪项陈述是正确的?
可以从聚合的事件中投影单个状态表示。
可以投影多个状态表示,但必须以支持多个投影的方式对领域事件进行建模。
可以投影多个状态表示,并且您可以在将来随时添加其他投影。
所有的陈述都是不正确的。
Which of the following statements is correct regarding the options of projecting state from a series of events?
A single state representation can be projected from an aggregate’s events.
Multiple state representations can be projected, but the domain events have to be modeled in a way that supports multiple projections.
Multiple state representations can be projected and you can always add additional projections in the future.
All of the statements are incorrect.
关于基于状态和事件源聚合之间的区别,以下哪项陈述是正确的?
事件源聚合可以产生领域事件,而基于状态的聚合不能产生领域事件。
聚合模式的两种变体都会产生领域事件,但只有事件源聚合使用领域事件作为事实来源。
事件源聚合确保为每个状态转换生成域事件。
B和C都正确。
Which of the following statements is correct regarding the difference between state-based and event-sourced aggregates?
An event-sourced aggregate can produce domain events, while a state-based aggregate cannot produce domain events.
Both variants of the aggregate pattern produce domain events, but only event-sourced aggregates use domain events as the source of truth.
Event-sourced aggregates ensure that domain events are generated for every state transition.
Both B and C are correct.
回到本书前言中描述的 WolfDesk 公司,该系统的哪些功能适合作为事件源域模型来实现?
Going back to the WolfDesk company described in the book’s Preface, which functionality of the system lends itself to be implemented as an event-sourced domain model?
1个小 FP 布鲁克斯 (1974)。人月神话:软件工程论文集。马萨诸塞州雷丁:Addison-Wesley。
1 Brooks, F. P. Jr. (1974). The Mythical Man-Month: Essays on Software Engineering. Reading, MA: Addison-Wesley.
2个例外情况除外,例如数据迁移。
2 Except for exceptional cases, such as data migration.
3个通用数据保护条例。(nd) 2021 年 6 月 14 日从维基百科检索。
3 General Data Protection Regulation. (n.d.) Retrieved June 14, 2021, from Wikipedia.
本书至此讨论的战术模式定义了建模和实现业务逻辑的不同方式。在本章中,我们将在更广泛的背景下探索战术设计决策:协调系统组件之间的交互和依赖关系的不同方法。
The tactical patterns discussed up to this point in the book defined the different ways to model and implement business logic. In this chapter, we will explore tactical design decisions in a broader context: the different ways to orchestrate the interactions and dependencies between a system’s components.
业务逻辑是软件最重要的部分;然而,它不是软件系统的唯一部分。为了实现功能性和非功能性需求,代码库必须承担更多责任。它必须与用户交互以收集输入并提供输出,并且必须使用不同的存储机制来保持状态并与外部系统和信息提供者集成。
Business logic is the most important part of software; however, it is not the only part of a software system. To implement functional and nonfunctional requirements, the codebase has to fulfill more responsibilities. It has to interact with users to gather input and provide output, and it has to use different storage mechanisms to persist state and integrate with external systems and information providers.
代码库必须处理的各种问题使得它的业务逻辑很容易分散到不同的组件中:也就是说,一些逻辑在用户界面或数据库中实现,或者在不同的组件中被复制。在实施问题上缺乏严格的组织使得代码库难以更改。当业务逻辑必须更改时,代码库的哪些部分必须受到更改的影响可能并不明显。更改可能会对系统看似无关的部分产生意想不到的影响。相反,可能很容易错过必须修改的代码。所有这些问题都极大地增加了维护代码库的成本。
The variety of concerns that a codebase has to take care of makes it easy for its business logic to become diffused among the different components: that is, for some of the logic to be implemented in the user interface or database, or be duplicated in different components. Lacking strict organization in implementation concerns makes the codebase hard to change. When the business logic has to change, it may not be evident what parts of the codebase have to be affected by the change. The change may have unexpected effects on seemingly unrelated parts of the system. Conversely, it may be easy to miss code that has to be modified. All of these issues dramatically increase the cost of maintaining the codebase.
架构模式为代码库的不同方面引入了组织原则,并在它们之间呈现清晰的界限:业务逻辑如何连接到系统的输入、输出和其他基础结构组件。这会影响这些组件如何相互交互:它们共享什么知识以及组件如何相互引用。
Architectural patterns introduce organizational principles for the different aspects of a codebase and present clear boundaries between them: how the business logic is wired to the system’s input, output, and other infrastructural components. This affects how these components interact with each other: what knowledge they share and how the components reference each other.
选择适当的方式来组织代码库,或选择正确的架构模式,对于在短期内支持业务逻辑的实施和在长期内减轻维护至关重要。让我们探索三种主要的应用程序架构模式及其用例:分层架构、端口和适配器以及 CQRS。
Choosing the appropriate way to organize the codebase, or the correct architectural pattern, is crucial to support implementation of the business logic in the short term and alleviate maintenance in the long term. Let’s explore three predominant application architecture patterns and their use cases: layered architecture, ports & adapters, and CQRS.
分层架构是最常见的架构模式之一。它将代码库组织成水平层,每一层解决以下技术问题之一:与消费者交互、实施业务逻辑和持久化数据。您可以在图 8-1中看到这一点。
Layered architecture is one of the most common architectural patterns. It organizes the codebase into horizontal layers, with each layer addressing one of the following technical concerns: interaction with the consumers, implementing business logic, and persisting the data. You can see this represented in Figure 8-1.
在其经典形式中,分层体系结构由三层组成:表示层 (PL)、业务逻辑层 (BLL) 和数据访问层 (DAL)。
In its classic form, the layered architecture consists of three layers: the presentation layer (PL), the business logic layer (BLL), and the data access layer (DAL).
如图 8-2所示,表示层实现了该程序的用户界面,用于与其消费者进行交互。在模式的原始形式中,该层表示图形界面,例如 Web 界面或桌面应用程序。
The presentation layer, shown in Figure 8-2, implements the program’s user interface for interactions with its consumers. In the pattern’s original form, this layer denotes a graphical interface, such as a web interface or a desktop application.
然而,在现代系统中,表示层具有更广泛的范围:即触发程序行为的所有方式,包括同步和异步。例如:
In modern systems, however, the presentation layer has a broader scope: that is, all means for triggering the program’s behavior, both synchronous and asynchronous. For example:
图形用户界面 (GUI)
Graphical user interface (GUI)
命令行界面 (CLI)
Command-line interface (CLI)
用于与其他系统进行编程集成的 API
API for programmatic integration with other systems
订阅消息代理中的事件
Subscription to events in a message broker
用于发布传出事件的消息主题
Message topics for publishing outgoing events
所有这些都是系统接收来自外部环境的请求并传达输出的手段。严格来说,表现层是程序的公共接口。
All of these are the means for the system to receive requests from the external environment and communicate the output. Strictly speaking, the presentation layer is the program’s public interface.
顾名思义,这一层负责实现并封装程序的业务逻辑。这是执行业务决策的地方。正如 Eric Evans 所说,1这一层是软件的核心。
As the name suggests, this layer is responsible for implementing and encapsulating the program’s business logic. This is the place where business decisions are implemented. As Eric Evans says,1 this layer is the heart of software.
该层是第5章到第 7章中描述的业务逻辑模式实现的地方——例如,活动记录或域模型(参见图 8-3)。
This layer is where the business logic patterns described in Chapters 5–7 are implemented—for example, active records or a domain model (see Figure 8-3).
数据访问层提供对持久性机制的访问。在模式的原始形式中,这指的是系统的数据库。然而,与表示层的情况一样,该层的职责对于现代系统来说更为广泛。
The data access layer provides access to persistence mechanisms. In the pattern’s original form, this referred to the system’s database. However, as in the case of the presentation layer, the layer’s responsibility is broader for modern systems.
首先,自从NoSQL革命爆发以来,一个系统与多个数据库一起工作是很常见的。例如,文档存储可以充当操作数据库、动态查询的搜索索引以及性能优化操作的内存数据库。
First, ever since the NoSQL revolution broke out, it is common for a system to work with multiple databases. For example, a document store can act as the operational database, a search index for dynamic queries, and an in-memory database for performance-optimized operations.
其次,传统数据库并不是存储信息的唯一媒介。例如,基于云的对象存储2可用于存储系统的文件,或者消息总线可用于协调程序不同功能之间的通信。3个
Second, traditional databases are not the only medium for storing information. For example, cloud-based object storage2 can be used to store the system’s files, or a message bus can be used to orchestrate communication between the program’s different functions.3
最后,该层还包括与实现程序功能所需的各种外部信息提供者的集成:外部系统提供的 API,或云供应商的托管服务,例如语言翻译、股票市场数据和音频转录(见图8- 4 ).
Finally, this layer also includes integration with the various external information providers needed to implement the program’s functionality: APIs provided by external systems, or cloud vendors’ managed services, such as language translation, stock market data, and audio transcription (see Figure 8-4).
这些层集成在自上而下的通信模型中:如图 8-5所示,每一层只能持有对其正下方层的依赖。这加强了实现关注点的解耦,并减少了各层之间共享的知识。在图 8-5中,表示层仅引用业务逻辑层。它不知道在数据访问层中做出的设计决策。
The layers are integrated in a top-down communication model: each layer can hold a dependency only on the layer directly beneath it, as shown in Figure 8-5. This enforces decoupling of implementation concerns and reduces the knowledge shared between the layers. In Figure 8-5, the presentation layer references only the business logic layer. It has no knowledge of the design decisions made in the data access layer.
分层架构模式扩展了一个附加层是很常见的:服务层。
It’s common to see the layered architecture pattern extended with an additional layer: the service layer.
使用服务层定义应用程序的边界建立一组可用操作并协调应用程序在每个操作中的响应。
—企业应用架构模式4
Defines an application’s boundary with a layer of services that establishes a set of available operations and coordinates the application’s response in each operation.
—Patterns of Enterprise Application Architecture4
The service layer acts as an intermediary between the program’s presentation and business logic layers. Consider the following code:
namespaceMvcApplication.Controllers{publicclassUserController:Controller{...[AcceptVerbs(HttpVerbs.Post)]publicActionResultCreate(ContactDetailscontactDetails){OperationResultresult=null;try{_db.StartTransaction();varuser=newUser();user.SetContactDetails(contactDetails)user.Save();_db.Commit();result=OperationResult.Success;}catch(Exceptionex){_db.Rollback();result=OperationResult.Exception(ex);}returnView(result);}}}
namespaceMvcApplication.Controllers{publicclassUserController:Controller{...[AcceptVerbs(HttpVerbs.Post)]publicActionResultCreate(ContactDetailscontactDetails){OperationResultresult=null;try{_db.StartTransaction();varuser=newUser();user.SetContactDetails(contactDetails)user.Save();_db.Commit();result=OperationResult.Success;}catch(Exceptionex){_db.Rollback();result=OperationResult.Exception(ex);}returnView(result);}}}
本例中的 MVC 控制器属于表现层。它公开了一个创建新用户的端点。端点使用用户活动记录对象创建一个新实例并保存它。此外,它协调数据库事务以确保在出现错误时生成正确的响应。
The MVC controller in this example belongs to the presentation layer. It exposes an endpoint that creates a new user. The endpoint uses the User active record object to create a new instance and save it. Moreover, it orchestrates a database transaction to ensure that a proper response is generated in case of an error.
为了进一步将表示层与底层业务逻辑分离,可以将此类编排逻辑移至服务层,如图8-6所示。
To further decouple the presentation layer from the underlying business logic, such orchestration logic can be moved into a service layer, as shown in Figure 8-6.
重要的是要注意,在架构模式的上下文中,服务层是一个逻辑边界。它不是物理服务。
It’s important to note that in the context of the architectural pattern, the service layer is a logical boundary. It is not a physical service.
服务层充当业务逻辑层:对外暴露一个接口,对应公共接口的方法,封装底层所需的编排。例如:
The service layer acts as a façade for the business logic layer: it exposes an interface that corresponds with the public interface’s methods, encapsulating the required orchestration of the underlying layers. For example:
interfaceCampaignManagementService{OperationResultCreateCampaign(CampaignDetailsdetails);OperationResultPublish(CampaignIdid,PublishingScheduleschedule);OperationResultDeactivate(CampaignIdid);OperationResultAddDisplayLocation(CampaignIdid,DisplayLocationnewLocation);...}
interfaceCampaignManagementService{OperationResultCreateCampaign(CampaignDetailsdetails);OperationResultPublish(CampaignIdid,PublishingScheduleschedule);OperationResultDeactivate(CampaignIdid);OperationResultAddDisplayLocation(CampaignIdid,DisplayLocationnewLocation);...}
以上方法都对应系统的公共接口。但是,它们缺少与表示相关的实现细节。表示层的职责仅限于向服务层提供所需的输入并将其响应传回给调用者。
All of the preceding methods correspond to the system’s public interface. However, they lack presentation-related implementation details. The presentation layer’s responsibility becomes limited to providing the required input to the service layer and communicating its responses back to the caller.
让我们重构前面的示例并将编排逻辑提取到服务层中:
Let’s refactor the preceding example and extract the orchestration logic into a service layer:
namespaceServiceLayer{publicclassUserService{...publicOperationResultCreate(ContactDetailscontactDetails){OperationResultresult=null;try{_db.StartTransaction();varuser=newUser();user.SetContactDetails(contactDetails)user.Save();_db.Commit();result=OperationResult.Success;}catch(Exceptionex){_db.Rollback();result=OperationResult.Exception(ex);}returnresult;}...}}namespaceMvcApplication.Controllers{publicclassUserController:Controller{...[AcceptVerbs(HttpVerbs.Post)]publicActionResultCreate(ContactDetailscontactDetails){varresult=_userService.Create(contactDetails);returnView(result);}}}
namespaceServiceLayer{publicclassUserService{...publicOperationResultCreate(ContactDetailscontactDetails){OperationResultresult=null;try{_db.StartTransaction();varuser=newUser();user.SetContactDetails(contactDetails)user.Save();_db.Commit();result=OperationResult.Success;}catch(Exceptionex){_db.Rollback();result=OperationResult.Exception(ex);}returnresult;}...}}namespaceMvcApplication.Controllers{publicclassUserController:Controller{...[AcceptVerbs(HttpVerbs.Post)]publicActionResultCreate(ContactDetailscontactDetails){varresult=_userService.Create(contactDetails);returnView(result);}}}
具有明确的服务级别有许多优点:
Having an explicit service level has a number of advantages:
我们可以复用同一个服务层来服务多个公共接口;例如,图形用户界面和 API。不需要复制编排逻辑。
We can reuse the same service layer to serve multiple public interfaces; for example, a graphical user interface and an API. No duplication of the orchestration logic is required.
它通过将所有相关方法集中在一个地方来提高模块化。
It improves modularity by gathering all related methods in one place.
它进一步解耦了表示层和业务逻辑层。
It further decouples the presentation and business logic layers.
它使测试业务功能变得更加容易。
It makes it easier to test the business functionality.
也就是说,服务层并不总是必需的。例如,当业务逻辑作为事务脚本实现时,它本质上是一个服务层,因为它已经公开了一组构成系统公共接口的方法。在这种情况下,服务层的 API 将只是重复交易脚本的公共接口,而不会抽象或封装任何复杂性。因此,服务层或业务逻辑层就足够了。
That said, a service layer is not always necessary. For example, when the business logic is implemented as a transaction script, it essentially is a service layer, as it already exposes a set of methods that form the system’s public interface. In such a case, the service layer’s API would just repeat the transaction scripts’ public interfaces, without abstracting or encapsulating any complexity. Hence, either a service layer or a business logic layer will suffice.
另一方面,如果业务逻辑模式需要外部编排,则需要服务层,例如活动记录模式。在这种情况下,服务层实现了事务脚本模式,而它所操作的活动记录位于业务逻辑层。
On the other hand, the service layer is required if the business logic pattern requires external orchestration, as in the case of the active record pattern. In this case, the service layer implements the transaction script pattern, while the active records it operates on are located in the business logic layer.
Elsewhere, you may encounter other terms used for the layered architecture:
表示层=用户界面层
Presentation layer = user interface layer
服务层=应用层
Service layer = application layer
业务逻辑层=领域层=模型层
Business logic layer = domain layer = model layer
数据访问层=基础设施层
Data access layer = infrastructure layer
为了消除混淆,我使用原始术语来展示模式。也就是说,我更喜欢“用户界面层”和“基础设施层”,因为这些术语更好地反映了现代系统和应用层的职责,以避免与服务的物理边界混淆。
To eliminate confusion, I present the pattern using the original terminology. That said, I prefer “user interface layer” and “infrastructure layer” as these terms better reflect the responsibilities of modern systems and an application layer to avoid confusion with the physical boundaries of services.
业务逻辑与数据之间的依赖关系访问层使这种架构模式非常适合使用事务脚本或活动记录模式实现其业务逻辑的系统。
The dependency between the business logic and the data access layers makes this architectural pattern a good fit for a system with its business logic implemented using the transaction script or active record pattern.
然而,该模式使得实现域模型变得具有挑战性。在域模型中,业务实体(聚合和值对象)应该没有依赖性,也不知道底层基础设施。分层架构的自上而下的依赖关系需要跳过一些环节才能满足此要求。仍然可以在分层架构中实现域模型,但我们接下来将讨论的模式更适合。
However, the pattern makes it challenging to implement a domain model. In a domain model, the business entities (aggregates and value objects) should have no dependency and no knowledge of the underlying infrastructure. The layered architecture’s top-down dependency requires jumping through some hoops to fulfill this requirement. It is still possible to implement a domain model in a layered architecture, but the pattern we will discuss next fits much better.
端口和适配器架构解决了分层架构的缺点,更适合实现更复杂的业务逻辑。有趣的是,这两种模式非常相似。让我们将分层架构“重构”为端口和适配器。
The ports & adapters architecture addresses the shortcomings of the layered architecture and is a better fit for implementation of more complex business logic. Interestingly, both patterns are quite similar. Let’s “refactor” the layered architecture into ports & adapters.
本质上,表示层和数据访问层都代表与外部组件集成:数据库、外部服务和用户界面框架。这些技术实现细节不反映系统的业务逻辑;因此,让我们将所有此类基础架构关注点统一到一个“基础架构层”中,如图8-8所示。
Essentially, both the presentation layer and data access layer represent integration with external components: databases, external services, and user interface frameworks. These technical implementation details do not reflect the system’s business logic; so, let’s unify all such infrastructural concerns into a single “infrastructure layer,” as shown in Figure 8-8.
依赖倒置原则(DIP)指出实现业务逻辑的高层模块不应该依赖低层模块。然而,这正是传统分层架构中发生的情况。业务逻辑层依赖于基础设施层。为了符合DIP,我们把关系倒过来,如图8-9所示。
The dependency inversion principle (DIP) states that high-level modules, which implement the business logic, should not depend on low-level modules. However, that’s precisely what happens in the traditional layered architecture. The business logic layer depends on the infrastructure layer. To conform with the DIP, let’s reverse the relationship, as shown in Figure 8-9.
现在,业务逻辑层不再夹在技术问题之间,而是发挥了核心作用。它不依赖于系统的任何基础结构组件。
Instead of being sandwiched between the technological concerns, now the business logic layer takes the central role. It doesn’t depend on any of the system’s infrastructural components.
最后,让我们添加一个应用程序5层作为系统的公共接口。作为分层架构中的服务层,它描述了系统暴露的所有操作,并编排了系统执行这些操作的业务逻辑。最终的架构如图 8-10所示。
Finally, let’s add an application5 layer as a façade for the system’s public interface. As the service layer in the layered architecture, it describes all the operations exposed by the system and orchestrates the system’s business logic for executing them. The resultant architecture is depicted in Figure 8-10.
图 8-10中描绘的体系结构是端口和适配器体系结构模式。业务逻辑不依赖于任何底层,这是实现域模型和事件源域模型模式所必需的。
The architecture depicted in Figure 8-10 is the ports & adapters architectural pattern. The business logic doesn’t depend on any of the underlying layers, as required for implementing the domain model and event-sourced domain model patterns.
为什么将此模式称为端口和适配器?要回答这个问题,让我们看看基础组件是如何与业务逻辑集成的。
Why is this pattern called ports & adapters? To answer this question, let’s see how the infrastructural components are integrated with the business logic.
端口和适配器架构的核心目标是解耦系统的业务逻辑来自其基础结构组件。
The core goal of the ports & adapters architecture is to decouple the system’s business logic from its infrastructural components.
业务逻辑层不是直接引用和调用基础结构组件,而是定义必须由基础结构层实现的“端口”。基础设施层实现“适配器”:用于使用不同技术的端口接口的具体实现(参见图 8-11)。
Instead of referencing and calling the infrastructural components directly, the business logic layer defines “ports” that have to be implemented by the infrastructure layer. The infrastructure layer implements “adapters”: concrete implementations of the ports’ interfaces for working with different technologies (see Figure 8-11).
通过依赖注入或引导,抽象端口被解析为基础设施层中的具体适配器。
The abstract ports are resolved into concrete adapters in the infrastructure layer, either through dependency injection or by bootstrapping.
例如,这是一个可能的端口定义和消息总线的具体适配器:
For example, here is a possible port definition and a concrete adapter for a message bus:
namespaceApp.BusinessLogicLayer{publicinterfaceIMessaging{voidPublish(Messagepayload);voidSubscribe(Messagetype,Actioncallback);}}namespaceApp.Infrastructure.Adapters{publicclassSQSBus:IMessaging{...}}
namespaceApp.BusinessLogicLayer{publicinterfaceIMessaging{voidPublish(Messagepayload);voidSubscribe(Messagetype,Actioncallback);}}namespaceApp.Infrastructure.Adapters{publicclassSQSBus:IMessaging{...}}
端口和适配器架构也称为六边形架构,洋葱架构和干净的架构。所有这些模式都基于相同的设计原则,具有相同的组件,并且它们之间具有相同的关系,但在分层架构的情况下,术语可能有所不同:
The ports & adapters architecture is also known as hexagonal architecture, onion architecture, and clean architecture. All of these patterns are based on the same design principles, have the same components, and have the same relationships between them, but as in the case of the layered architecture, the terminology may differ:
应用层=服务层=用例层
Application layer = service layer = use case layer
业务逻辑层=领域层=核心层
Business logic layer = domain layer = core layer
尽管如此,这些模式可能会被错误地视为概念上的不同。这只是无处不在的语言的重要性的另一个例子。
Despite that, these patterns can be mistakenly treated as conceptually different. That’s just another example of the importance of a ubiquitous language.
命令查询责任分离 (CQRS) 模式基于与端口和适配器相同的业务逻辑和基础设施问题的组织原则。但是,它的不同之处在于系统数据的管理方式。此模式支持在多个持久模型中表示系统数据。
The command-query responsibility segregation (CQRS) pattern is based on the same organizational principles for business logic and infrastructural concerns as ports & adapters. It differs, however, in the way the system’s data is managed. This pattern enables representation of the system’s data in multiple persistent models.
让我们看看为什么我们可能需要这样的解决方案以及如何实施它。
Let’s see why we might need such a solution and how to implement it.
在许多情况下,即使不是不可能,也可能很难使用单一模型系统的业务领域来解决系统的所有需求。例如,如第 7 章所述,联机事务处理 (OLTP) 和联机分析处理 (OLAP) 可能需要系统数据的不同表示。
In many cases, it may be difficult, if not impossible, to use a single model of the system’s business domain to address all of the system’s needs. For example, as discussed in Chapter 7, online transaction processing (OLTP) and online analytical processing (OLAP) may require different representations of the system’s data.
使用多个模型的另一个原因可能与多语言持久性的概念有关。没有完美的数据库。或者,正如 Greg Young 6所说,所有数据库都存在缺陷,每个数据库都有自己的缺陷:我们通常必须平衡规模、一致性或支持的查询模型的需求。寻找完美数据库的另一种方法是多语言持久性模型:使用多个数据库来实现不同的数据相关需求。例如,单个系统可能使用文档存储作为其操作数据库,使用列存储进行分析/报告,以及使用搜索引擎来实现强大的搜索功能。
Another reason for working with multiple models may have to do with the notion of polyglot persistence. There is no perfect database. Or, as Greg Young6 says, all databases are flawed, each in its own way: we often have to balance the needs for scale, consistency, or supported querying models. An alternative to finding a perfect database is the polyglot persistence model: using multiple databases to implement different data-related requirements. For example, a single system might use a document store as its operational database, a column store for analytics/reporting, and a search engine for implementing robust search capabilities.
最后,CQRS 模式与事件溯源密切相关。最初,定义 CQRS 是为了解决事件源模型的有限查询可能性:一次只能查询一个聚合实例的事件。CQRS 模式提供了将投影模型具体化为可用于灵活查询选项的物理数据库的可能性。
Finally, the CQRS pattern is closely related to event sourcing. Originally, CQRS was defined to address the limited querying possibilities of an event-sourced model: it is only possible to query events of one aggregate instance at a time. The CQRS pattern provides the possibility of materializing projected models into physical databases that can be used for flexible querying options.
也就是说,本章将 CQRS 从事件溯源中“解耦”。我打算表明即使业务逻辑是使用任何其他业务逻辑实现模式实现的,CQRS 也是有用的。
That said, this chapter “decouples” CQRS from event sourcing. I intend to show that CQRS is useful even if the business logic is implemented using any of the other business logic implementation patterns.
让我们看看 CQRS 如何允许使用多种存储机制来表示系统数据的不同模型。
Let’s see how CQRS allows the use of multiple storage mechanisms for representing different models of the system’s data.
顾名思义,该模式分离了职责系统的模型。有两种类型的模型:命令执行模型和读取模型。
As the name suggests, the pattern segregates the responsibilities of the system’s models. There are two types of models: the command execution model and the read models.
CQRS 使用单一模型来执行修改系统状态的操作(系统命令)。该模型用于实现业务逻辑、验证规则和执行不变量。
CQRS devotes a single model to executing operations that modify the system’s state (system commands). This model is used to implement the business logic, validate rules, and enforce invariants.
命令执行模型也是唯一代表强一致性数据(系统的真实来源)的模型。应该可以读取业务实体的强一致状态,并在更新时具有乐观并发支持。
The command execution model is also the only model representing strongly consistent data—the system’s source of truth. It should be possible to read the strongly consistent state of a business entity and have optimistic concurrency support when updating it.
系统可以根据需要定义尽可能多的模型,以向用户呈现数据或向其他系统提供信息。
The system can define as many models as needed to present data to users or supply information to other systems.
读取模型是预缓存的投影。它可以驻留在持久数据库、平面文件或内存缓存中。正确实施 CQRS 可以清除投影的所有数据并从头开始重新生成。这也使得能够在未来使用额外的预测来扩展系统——最初无法预见的模型。
A read model is a precached projection. It can reside in a durable database, flat file, or in-memory cache. Proper implementation of CQRS allows for wiping out all data of a projection and regenerating it from scratch. This also enables extending the system with additional projections in the future—models that couldn’t have been foreseen originally.
最后,读取模型是只读的。系统的任何操作都不能直接修改读取模型的数据。
Finally, read models are read-only. None of the system’s operations can directly modify the read models’ data.
为了使读取的模型起作用,系统必须预测更改从命令执行模型到它的所有读取模型。图 8-12说明了这个概念。
For the read models to work, the system has to project changes from the command execution model to all its read models. This concept is illustrated in Figure 8-12.
读取模型的投影类似于关系数据库中物化视图的概念:无论何时更新源表,更改都必须反映在预缓存视图中。
The projection of read models is similar to the notion of a materialized view in relational databases: whenever source tables are updated, the changes have to be reflected in the precached views.
接下来,让我们看看两种生成投影的方法:同步和异步。
Next, let’s see two ways to generate projections: synchronously and asynchronously.
Synchronous projections fetch changes to the OLTP data through the catch-up subscription model:
投影引擎在上次处理的检查点之后查询 OLTP 数据库以获取添加或更新的记录。
The projection engine queries the OLTP database for added or updated records after the last processed checkpoint.
投影引擎使用更新的数据来重新生成/更新系统的读取模型。
The projection engine uses the updated data to regenerate/update the system’s read models.
投影引擎存储最后处理的记录的检查点。该值将在下一次迭代期间用于获取在最后处理的记录之后添加或修改的记录。
The projection engine stores the checkpoint of the last processed record. This value will be used during the next iteration for getting records added or modified after the last processed record.
这个过程在图 8-13中进行了说明,并在图 8-14中显示为时序图。
This process is illustrated in Figure 8-13 and shown as a sequence diagram in Figure 8-14.
为了使追赶订阅起作用,命令执行模型必须检查所有附加或更新的数据库记录。存储机制还应该支持基于检查点的记录查询。
For the catch-up subscription to work, the command execution model has to checkpoint all the appended or updated database records. The storage mechanism should also support the querying of records based on the checkpoint.
检查点可以使用数据库的特性来实现。例如,SQL Server 的“rowversion”列可用于在插入或更新行时生成唯一的递增数字,如图8-15所示。在缺少此类功能的数据库中,可以实施自定义解决方案,增加运行计数器并将其附加到每个修改的记录。确保基于检查点的查询返回一致的结果很重要。如果最后返回的记录的检查点值为 10,则在下一次执行时,新请求的值不应低于 10。否则,投影引擎将跳过这些记录,从而导致模型不一致。
The checkpoint can be implemented using the databases’ features. For example, SQL Server’s “rowversion” column can be used to generate unique, incrementing numbers upon inserting or updating a row, as illustrated in Figure 8-15. In databases that lack such functionality, a custom solution can be implemented that increments a running counter and appends it to each modified record. It’s important to ensure that the checkpoint-based query returns consistent results. If the last returned record has a checkpoint value of 10, on the next execution no new requests should have values lower than 10. Otherwise, these records will be skipped by the projection engine, which will result in inconsistent models.
同步投影方法使得添加新投影和从头重新生成现有投影变得微不足道。在后一种情况下,您所要做的就是将检查点重置为 0;投影引擎将扫描记录并从头开始重建投影。
The synchronous projection method makes it trivial to add new projections and regenerate existing ones from scratch. In the latter case, all you have to do is reset the checkpoint to 0; the projection engine will scan the records and rebuild the projections from the ground up.
异步投影场景下,命令执行模型将所有提交的更改发布到消息总线。系统的投影引擎可以订阅发布的消息,并使用它们来更新读取模型,如图8-16所示。
In the asynchronous projection scenario, the command execution model publishes all committed changes to a message bus. The system’s projection engines can subscribe to the published messages and use them to update the read models, as shown in Figure 8-16.
尽管具有明显的缩放和性能优势异步投影方式,更容易受到分布式计算的挑战。如果消息被乱序处理或重复,不一致的数据将被投射到读取模型中。
Despite the apparent scaling and performance advantages of the asynchronous projection method, it is more prone to the challenges of distributed computing. If the messages are processed out of order or duplicated, inconsistent data will be projected into the read models.
这种方法还使添加新投影或重新生成现有投影更具挑战性。
This method also makes it more challenging to add new projections or regenerate existing ones.
出于这些原因,建议始终实施同步投影,并可选择在同步投影之上实施额外的异步投影。
For these reasons, it’s advisable to always implement synchronous projection and, optionally, an additional asynchronous projection on top of it.
在 CQRS 架构中,系统模型的职责根据它们的类型被隔离。一个命令只能运行在强一致性命令执行模型上。查询不能直接修改系统的任何持久状态——无论是读取模型还是命令执行模型。
In the CQRS architecture, the responsibilities of the system’s models are segregated according to their type. A command can only operate on the strongly consistent command execution model. A query cannot directly modify any of the system’s persisted state—neither the read models nor the command execution model.
关于基于 CQRS 的系统的一个常见误解是命令只能修改数据,并且只能通过读取模型获取数据以供显示。换句话说,执行方法的命令不应该返回任何数据。这是错误的。这种方法会产生意外的复杂性并导致糟糕的用户体验。
A common misconception about CQRS-based systems is that a command can only modify data, and data can be fetched for display only through a read model. In other words, the command executing the methods should never return any data. This is wrong. This approach produces accidental complexities and leads to a bad user experience.
命令应该始终让调用者知道它是成功还是失败。如果失败了,为什么会失败?是否存在验证或技术问题?调用者必须知道如何修复命令。因此,命令可以——而且在许多情况下应该——返回数据;例如,如果系统的用户界面必须反映命令产生的修改。这不仅使消费者更容易使用系统,因为他们会立即收到对其操作的反馈,而且返回的值可以在消费者的工作流程中进一步使用,从而消除了不必要的数据往返的需要。
A command should always let the caller know whether it has succeeded or failed. If it has failed, why did it fail? Was there a validation or technical issue? The caller has to know how to fix the command. Therefore, a command can—and in many cases should—return data; for example, if the system’s user interface has to reflect the modifications resulting from the command. Not only does this make it easier for consumers to work with the system since they immediately receive feedback for their actions, but the returned values can be used further in the consumers’ workflows, eliminating the need for unnecessary data round trips.
这里唯一的限制是返回的数据应该来自强一致性模型——命令执行模型——因为我们不能期望最终一致的投影立即被刷新。
The only limitation here is that the returned data should originate from the strongly consistent model—the command execution model—as we cannot expect the projections, which will eventually be consistent, to be refreshed immediately.
CQRS 模式对于需要正常工作的应用程序很有用在多个模型中具有相同的数据,可能存储在不同类型的数据库中。从操作的角度来看,该模式支持领域驱动设计的核心价值,即为手头的任务使用最有效的模型,并不断改进业务领域的模型。从基础架构的角度来看,CQRS 允许利用不同类型数据库的优势;例如,使用关系数据库来存储命令执行模型、用于全文搜索的搜索索引以及用于快速数据检索的预呈现平面文件,所有存储机制都可靠地同步。
The CQRS pattern can be useful for applications that need to work with the same data in multiple models, potentially stored in different kinds of databases. From an operational perspective, the pattern supports domain-driven design’s core value of working with the most effective models for the task at hand, and continuously improving the model of the business domain. From an infrastructural perspective, CQRS allows for leveraging the strength of the different kinds of databases; for example, using a relational database to store the command execution model, a search index for full text search, and prerendered flat files for fast data retrieval, with all the storage mechanisms reliably synchronized.
此外,CQRS 自然适用于事件源领域模型。事件溯源模型使得无法根据聚合的状态查询记录,但 CQRS 通过将状态投射到可查询的数据库中来实现这一点。
Moreover, CQRS naturally lends itself to event-sourced domain models. The event-sourcing model makes it impossible to query records based on the aggregates’ states, but CQRS enables this by projecting the states into queryable databases.
我们讨论过的模式——分层架构、端口和适配器架构,和 CQRS——不应被视为系统范围的组织原则。这些也不一定是整个限界上下文的高级架构模式。
The patterns we’ve discussed—layered architecture, ports & adapters architecture, and CQRS—should not be treated as systemwide organizational principles. These are not necessarily high-level architecture patterns for a whole bounded context either.
考虑包含多个子域的限界上下文,如图8-17所示。子域可以是不同的类型:核心、支持或通用。即使是相同类型的子域也可能需要不同的业务逻辑和架构模式(这是第 10 章的主题)。实施单一的、有界的、上下文范围的架构会无意中导致意外的复杂性。
Consider a bounded context encompassing multiple subdomains, as shown in Figure 8-17. The subdomains can be of different types: core, supporting, or generic. Even subdomains of the same type may require different business logic and architectural patterns (that’s the topic of Chapter 10). Enforcing a single, bounded, contextwide architecture will inadvertently lead to accidental complexity.
我们的目标是根据实际需求和业务战略来推动设计决策。除了水平划分系统的层之外,我们还可以引入额外的垂直划分。如图 8-18所示,为封装不同业务子域的模块定义逻辑边界并为每个模块使用适当的工具至关重要。
Our goal is to drive design decisions according to the actual needs and business strategy. In addition to the layers that partition the system horizontally, we can introduce additional vertical partitioning. It’s crucial to define logical boundaries for modules encapsulating distinct business subdomains and use the appropriate tools for each, as demonstrated in Figure 8-18.
适当的垂直边界使一个单一的有界上下文成为一个模块化的上下文,并有助于防止它变成一个大泥球。正如我们将在第 11 章中讨论的那样,这些逻辑边界稍后可以重构为更细粒度的有界上下文的物理边界。
Appropriate vertical boundaries make a monolithic bounded context a modular one and help to prevent it from becoming a big ball of mud. As we will discuss in Chapter 11, these logical boundaries can be refactored later into physical boundaries of finer-grained bounded contexts.
分层架构根据其技术关注点分解代码库。由于此模式将业务逻辑与数据访问实现相结合,因此非常适合基于活动记录的系统。
The layered architecture decomposes the codebase based on its technological concerns. Since this pattern couples business logic with data access implementation, it’s a good fit for active record–based systems.
端口和适配器架构颠倒了关系:它将业务逻辑置于中心并将其与所有基础架构依赖项分离。这种模式非常适合用域模型模式实现的业务逻辑。
The ports & adapters architecture inverts the relationships: it puts the business logic at the center and decouples it from all infrastructural dependencies. This pattern is a good fit for business logic implemented with the domain model pattern.
CQRS 模式表示多个模型中的相同数据。尽管此模式对于基于事件源域模型的系统是强制性的,但它也可以用于任何需要使用多种持久模型的方式的系统。
The CQRS pattern represents the same data in multiple models. Although this pattern is obligatory for systems based on the event-sourced domain model, it can also be used in any systems that need a way of working with multiple persistent models.
我们将在下一章讨论的模式从不同的角度解决架构问题:如何在系统的不同组件之间实现可靠的交互。
The patterns we will discuss in the next chapter address architectural concerns from a different perspective: how to implement reliable interaction between different components of a system.
哪些所讨论的架构模式可以与作为活动记录模式实现的业务逻辑一起使用?
分层架构
端口和适配器
CQRS
甲乙丙
Which of the discussed architectural patterns can be used with business logic implemented as the active record pattern?
Layered architecture
Ports & adapters
CQRS
A and C
所讨论的架构模式中的哪一个将业务逻辑与基础设施问题分离开来?
分层架构
端口和适配器
CQRS
乙丙
Which of the discussed architectural patterns decouples the business logic from infrastructural concerns?
Layered architecture
Ports & adapters
CQRS
B and C
假设您正在实施端口和适配器模式并且需要集成云提供商的托管消息总线。集成应该在哪一层实现?
业务逻辑层
应用层
基础设施层
任意层
Assume you are implementing the ports & adapters pattern and need to integrate a cloud provider’s managed message bus. In which layer should the integration be implemented?
Business logic layer
Application layer
Infrastructure layer
Any layer
关于 CQRS 模式,以下哪项陈述是正确的?
异步投影更容易扩展。
可以使用同步或异步投影,但不能同时使用。
命令不能向调用者返回任何信息。调用者应始终使用读取模型来获取执行操作的结果。
只要命令源自强一致性模型,它就可以返回信息。
A和D。
Which of the following statements is true regarding the CQRS pattern?
Asynchronous projections are easier to scale.
Either synchronous or asynchronous projection can be used, but not both at the same time.
A command cannot return any information to the caller. The caller should always use the read models to get the results of the executed actions.
A command can return information as long as it originates from a strongly consistent model.
A and D.
CQRS 模式允许在多个持久模型中表示相同的业务对象,因此允许在相同的限界上下文中使用多个模型。它是否与限界上下文作为模型边界的概念相矛盾?
The CQRS pattern allows for representing the same business objects in multiple persistent models, and thus allows working with multiple models in the same bounded context. Does it contradict the bounded context’s notion of being a model boundary?
1个Evans, E. (2003)。领域驱动设计:解决软件核心的复杂性。波士顿:Addison-Wesley。
1 Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Boston: Addison-Wesley.
2个例如 AWS S3 或谷歌云存储。
2 Such as AWS S3 or Google Cloud Storage.
3个在这种情况下,消息总线用于满足系统的内部需求。如果它被公开暴露,它就属于表现层。
3 In this context, the message bus is used for the system’s internal needs. If it were exposed publicly, it would belong to the presentation layer.
4个福勒,M. (2002)。企业应用架构模式。波士顿:Addison-Wesley。
4 Fowler, M. (2002). Patterns of Enterprise Application Architecture. Boston: Addison-Wesley.
5个由于我们不在分层架构的上下文中,我将自由使用术语应用程序层而不是服务层,因为它更好地反映了目的。
5 Since we are not in the context of the layered architecture, I will take the freedom to use the term application layer instead of service layer, as it better reflects the purpose.
6个 Greg Young 的多语言数据。(nd). 2021 年 6 月 14 日从YouTube检索。
6 Polyglot data by Greg Young. (n.d.). Retrieved June 14, 2021, from YouTube.
第 5章到第 8章介绍了战术设计模式,这些模式定义了实现系统组件的不同方式:如何对业务逻辑建模以及如何在架构上组织限界上下文的内部结构。在本章中,我们将超越单个组件的界限,讨论跨系统元素组织通信流的模式。
Chapters 5–8 presented tactical design patterns that define the different ways to implement a system’s components: how to model the business logic and how to organize the internals of a bounded context architecturally. In this chapter, we will step beyond the boundaries of a single component and discuss the patterns for organizing the flow of communication across a system’s elements.
您将在本章中学习的模式促进跨界上下文通信,解决聚合设计原则强加的限制,并协调跨多个系统组件的业务流程。
The patterns you will learn about in this chapter facilitate cross-bounded context communication, address the limitations imposed by aggregate design principles, and orchestrate business processes spanning multiple system components.
有界上下文是模型的边界——一种无处不在的语言。正如您在第 3 章中了解到的,可以使用不同的模式来设计跨不同限界上下文的通信。假设实现两个限界上下文的团队正在有效地沟通并愿意协作。在这种情况下,限界上下文可以在伙伴关系中集成:协议可以以临时方式进行协调,任何集成问题都可以通过团队之间的沟通得到有效解决。另一种合作驱动的集成方法是共享内核:团队提取并共同进化模型的有限部分;例如,将限界上下文的集成契约提取到共同拥有的存储库中。
A bounded context is the boundary of a model—a ubiquitous language. As you learned in Chapter 3, there are different patterns for designing communication across different bounded contexts. Suppose the teams implementing two bounded contexts are communicating effectively and willing to collaborate. In this case, the bounded contexts can be integrated in a partnership: the protocols can be coordinated in an ad hoc manner, and any integration issues can be effectively addressed through communication between the teams. Another cooperation-driven integration method is shared kernel: the teams extract and co-evolve a limited portion of a model; for example, extracting the bounded contexts’ integration contracts into a co-owned repository.
在客户-供应商关系中,权力的平衡倾向于上游(供应商)或下游(消费者)有界上下文。假设下游限界上下文不能符合上游限界上下文的模型。在这种情况下,需要更精细的技术解决方案,通过翻译限界上下文的模型来促进通信。
In a customer–supplier relationship, the balance of power tips toward either the upstream (supplier) or the downstream (consumer) bounded context. Suppose the downstream bounded context cannot conform to the upstream bounded context’s model. In this case, a more elaborate technical solution is required that can facilitate communication by translating the bounded contexts’ models.
这种转换可以由一方或有时双方处理:下游有界上下文可以使用反腐败层 (ACL) 使上游有界上下文的模型适应其需要,而上游有界上下文可以充当开放主机服务( OHS)并通过使用特定于集成的已发布语言来保护其消费者免受其实施模型更改的影响。由于反腐败层和开放主机服务的翻译逻辑相似,因此本章涵盖了实现选项而不区分模式,并且仅在特殊情况下提及差异。
This translation can be handled by one, or sometimes both, sides: the downstream bounded context can adapt the upstream bounded context’s model to its needs using an anticorruption layer (ACL), while the upstream bounded context can act as an open-host service (OHS) and protect its consumers from changes to its implementation model by using an integration-specific published language. Since the translation logic is similar for both the anticorruption layer and the open-host service, this chapter covers the implementation options without differentiating between the patterns and mentions the differences only in exceptional cases.
模型的翻译逻辑可以是无状态的或有状态的。 无状态转换会在发出传入 (OHS) 或传出 (ACL) 请求时即时发生,而有状态转换涉及更复杂的转换逻辑,需要数据库。让我们看看实现这两种类型的模型转换的设计模式。
The model’s translation logic can be either stateless or stateful. Stateless translation happens on the fly, as incoming (OHS) or outgoing (ACL) requests are issued, while stateful translation involves a more complicated translation logic that requires a database. Let’s see design patterns for implementing both types of model translation.
对于无状态模型转换,拥有转换的限界上下文(上游为 OHS,下游为 ACL)实施代理设计模式以插入传入和传出请求并将源模型映射到限界上下文的目标模型。这在图 9-1中描述。
For stateless model translation, the bounded context that owns the translation (OHS for upstream, ACL for downstream) implements the proxy design pattern to interject the incoming and outgoing requests and map the source model to the bounded context’s target model. This is depicted in Figure 9-1.
代理的实现取决于限界上下文是同步通信还是异步通信。
Implementation of the proxy depends on whether the bounded contexts are communicating synchronously or asynchronously.
翻译同步通信中使用的模型的典型方法是将转换逻辑嵌入到限界上下文的代码库中,如图9-2所示。在开放主机服务中,翻译为公共语言发生在处理传入请求时,而在反腐败层中,翻译发生在调用上游限界上下文时。
The typical way to translate models used in synchronous communication is to embed the transformation logic in the bounded context’s codebase, as shown in Figure 9-2. In an open-host service, translation to the public language takes place when processing incoming requests, and in an anticorruption layer, it occurs when calling the upstream bounded context.
在某些情况下,将转换逻辑卸载到外部组件(如 API 网关模式)可能更经济、更方便。API 网关组件可以是基于开源软件的解决方案,例如 Kong 或 KrakenD,也可以是云供应商的托管服务,例如 AWS API Gateway、Google Apigee 或 Azure API Management。
In some cases, it can be more cost-effective and convenient to offload the translation logic to an external component such as an API gateway pattern. The API gateway component can be an open source software-based solution such as Kong or KrakenD, or it can be a cloud vendor’s managed service such as AWS API Gateway, Google Apigee, or Azure API Management.
对于实现开放主机模式的限界上下文,API 网关负责将内部模型转换为集成优化的发布语言。此外,拥有显式 API 网关可以减轻管理和服务多个版本限界上下文 API 的过程,如图9-3所示。
For bounded contexts implementing the open-host pattern, the API gateway is responsible for converting the internal model into the integration-optimized published language. Moreover, having an explicit API gateway can alleviate the process of managing and serving multiple versions of the bounded context’s API, as depicted in Figure 9-3.
使用 API 网关实现的反腐败层可以被多个下游有界上下文使用。在这种情况下,反腐败层充当特定于集成的限界上下文,如图9-4所示。
Anticorruption layers implemented using an API gateway can be consumed by multiple downstream bounded contexts. In such cases, the anticorruption layer acts as an integration-specific bounded context, as shown in Figure 9-4.
这种有界上下文主要负责转换模型以便其他组件更方便地使用,通常称为交换上下文。
Such bounded contexts, which are mainly in charge of transforming models for more convenient consumption by other components, are often referred to as interchange contexts.
翻译异步通信中使用的模型您可以实现一个消息代理:一个订阅来自源限界上下文的消息的中间组件。代理将应用所需的模型转换并将生成的消息转发给目标订阅者(见图9-5)。
To translate models used in asynchronous communication you can implement a message proxy: an intermediary component subscribing to messages coming from the source bounded context. The proxy will apply the required model transformations and forward the resultant messages to the target subscriber (see Figure 9-5).
除了翻译消息的模型之外,拦截组件还可以通过过滤掉不相关的消息来减少目标限界上下文中的噪声。
In addition to translating the messages’ model, the intercepting component can also reduce the noise on the target bounded context by filtering out irrelevant messages.
实现开放主机服务时,异步模型转换必不可少。为模型的对象设计和公开发布语言并允许按原样发布领域事件,从而公开限界上下文的实现模型,这是一个常见的错误。异步翻译可用于拦截领域事件并将其转换为已发布的语言,从而更好地封装限界上下文的实现细节(见图9-6)。
Asynchronous model translation is essential when implementing an open host service. It’s a common mistake to design and expose a published language for the model’s objects and allow domain events to be published as they are, thereby exposing the bounded context’s implementation model. Asynchronous translation can be used to intercept the domain events and convert them into a published language, thus providing better encapsulation of the bounded context’s implementation details (see Figure 9-6).
此外,将消息翻译成已发布的语言可以使区分专为有界上下文的内部需求而设计的私有事件和为与其他有界上下文集成而设计的公共事件。我们将在第 15 章重新讨论和扩展私有/公共事件的主题,讨论领域驱动设计和事件驱动架构之间的关系。
Moreover, translating messages to the published language enables differentiating between private events that are intended for the bounded context’s internal needs and public events that are designed for integration with other bounded contexts. We’ll revisit and expand on the topic of private/public events in Chapter 15, where we discuss the relationship between domain-driven design and event-driven architecture.
对于更重要的模型转换——对于例如,当翻译机制必须聚合源数据或将来自多个来源的数据统一到一个模型中时——可能需要有状态的翻译。让我们详细讨论这些用例中的每一个。
For more significant model transformations—for example, when the translation mechanism has to aggregate the source data or unify data from multiple sources into a single model—a stateful translation may be required. Let’s discuss each of these use cases in detail.
假设有界上下文有兴趣聚合传入请求并批量处理它们以优化性能。在这种情况下,同步和异步请求都可能需要聚合(见图9-7)。
Let’s say a bounded context is interested in aggregating incoming requests and processing them in batches for performance optimization. In this case, aggregation may be required both for synchronous and asynchronous requests (see Figure 9-7).
源数据聚合的另一个常见用例是将多个细粒度消息组合成包含统一数据的单个消息,如图9-8所示。
Another common use case for aggregation of source data is combining multiple fine-grained messages into a single message containing the unified data, as depicted in Figure 9-8.
聚合传入数据的模型转换不能使用 API 网关来实现,因此需要更精细、有状态的处理。为了跟踪传入的数据并对其进行相应的处理,翻译逻辑需要自己的持久存储(见图9-9)。
Model transformation that aggregates incoming data cannot be implemented using an API gateway, and thus requires more elaborate, stateful processing. To track the incoming data and process it accordingly, the translation logic requires its own persistent storage (see Figure 9-9).
在某些用例中,您可以通过使用现成的产品来避免为有状态翻译实施自定义解决方案;例如,流处理平台(Kafka、AWS Kinesis 等)或批处理解决方案(Apache NiFi、AWS Glue、Spark 等)。
In some use cases, you can avoid implementing a custom solution for a stateful translation by using off-the-shelf products; for example, a stream-process platform (Kafka, AWS Kinesis, etc.), or a batching solution (Apache NiFi, AWS Glue, Spark, etc.).
有界上下文可能需要处理来自多个来源的数据聚合,包括其他有界上下文。一个典型的例子是后端换前端模式,1其中用户界面必须组合来自多个服务的数据。
A bounded context may need to process data aggregates from multiple sources, including other bounded contexts. A typical example for this is the backend-for-frontend pattern,1 in which the user interface has to combine data originating from multiple services.
另一个例子是限界上下文,它必须处理来自多个其他上下文的数据并实施复杂的业务逻辑来处理所有数据。在这种情况下,通过在有界上下文前面放置一个聚合来自所有其他有界上下文的数据的反腐败层,可以有利于分离集成和业务逻辑的复杂性,如图 9-10所示。
Another example is a bounded context that must process data from multiple other contexts and implement complex business logic to process all the data. In this case, it can be beneficial to decouple the integration and business logic complexities by fronting the bounded context with an anticorruption layer that aggregates data from all other bounded contexts, as shown in Figure 9-10.
在第 6 章中,我们讨论了其中的一个聚合与系统其余部分通信的方式是发布域事件。外部组件可以订阅这些域事件并执行它们的逻辑。但是域事件是如何发布到消息总线的呢?
In Chapter 6, we discussed that one of the ways aggregates communicate with the rest of the system is by publishing domain events. External components can subscribe to these domain events and execute their logic. But how are domain events published to a message bus?
在我们找到解决方案之前,让我们检查一下事件发布过程中的一些常见错误以及每种方法的后果。考虑以下代码:
Before we get to the solution, let’s examine a few common mistakes in the event publishing process and the consequences of each approach. Consider the following code:
01publicclassCampaign02{03...04List<DomainEvent>_events;05IMessageBus_messageBus;06...0708publicvoidDeactivate(stringreason)09{10for(lin_locations.Values())11{12l.Deactivate();13}1415IsActive=false;1617varnewEvent=newCampaignDeactivated(_id,reason);18_events.Append(newEvent);19_messageBus.Publish(newEvent);20}21}
01publicclassCampaign02{03...04List<DomainEvent>_events;05IMessageBus_messageBus;06...0708publicvoidDeactivate(stringreason)09{10for(lin_locations.Values())11{12l.Deactivate();13}1415IsActive=false;1617varnewEvent=newCampaignDeactivated(_id,reason);18_events.Append(newEvent);19_messageBus.Publish(newEvent);20}21}
在第 17 行,实例化了一个新事件。在接下来的两行中,它被附加到域事件聚合的内部列表(第 18 行),并且事件被发布到消息总线(第 19 行)。这种发布域事件的实现很简单但错误。出于两个原因,直接从聚合中发布领域事件是不好的。首先,事件将在聚合的新状态提交到数据库之前被分派。订户可能会收到活动已停用的通知,但这会与活动的状态相矛盾。其次,如果数据库事务由于竞争条件、后续聚合逻辑导致操作无效或仅仅是数据库中的技术问题而未能提交怎么办?即使数据库事务被回滚,
On line 17, a new event is instantiated. On the following two lines, it is appended to the aggregate’s internal list of domain events (line 18), and the event is published to the message bus (line 19). This implementation of publishing domain events is simple but wrong. Publishing the domain event right from the aggregate is bad for two reasons. First, the event will be dispatched before the aggregate’s new state is committed to the database. A subscriber may receive the notification that the campaign was deactivated, but it would contradict the campaign’s state. Second, what if the database transaction fails to commit because of a race condition, subsequent aggregate logic rendering the operation invalid, or simply a technical issue in the database? Even though the database transaction is rolled back, the event is already published and pushed to subscribers, and there is no way to retract it.
让我们试试别的:
Let’s try something else:
01publicclassManagementAPI02{03...04privatereadonlyIMessageBus_messageBus;05privatereadonlyICampaignRepository_repository;06...07publicExecutionResultDeactivateCampaign(CampaignIdid,stringreason)08{09try10{11varcampaign=repository.Load(id);12campaign.Deactivate(reason);13_repository.CommitChanges(campaign);1415varevents=campaign.GetUnpublishedEvents();16for(IDomainEventeinevents)17{18_messageBus.publish(e);19}20campaign.ClearUnpublishedEvents();21}22catch(Exceptionex)23{24...25}26}27}
01publicclassManagementAPI02{03...04privatereadonlyIMessageBus_messageBus;05privatereadonlyICampaignRepository_repository;06...07publicExecutionResultDeactivateCampaign(CampaignIdid,stringreason)08{09try10{11varcampaign=repository.Load(id);12campaign.Deactivate(reason);13_repository.CommitChanges(campaign);1415varevents=campaign.GetUnpublishedEvents();16for(IDomainEventeinevents)17{18_messageBus.publish(e);19}20campaign.ClearUnpublishedEvents();21}22catch(Exceptionex)23{24...25}26}27}
在前面的清单中,发布新领域事件的责任转移到了应用层。在第 11 到 13 行,聚合的相关实例Campaign被加载,它的Deactivate命令被执行,只有在更新状态成功提交到数据库之后,在第 15 到 20 行,新的域事件才会发布到消息总线。我们可以信任这个代码吗?不。
In the preceding listing, the responsibility of publishing new domain events is shifted to the application layer. On lines 11 through 13, the relevant instance of the Campaign aggregate is loaded, its Deactivate command is executed, and only after the updated state is successfully committed to the database, on lines 15 through 20, are the new domain events published to the message bus. Can we trust this code? No.
在这种情况下,运行逻辑的进程由于某种原因无法发布域事件。也许消息总线已关闭。或者运行代码的服务器在提交数据库事务后立即失败,但在发布事件之前系统仍会以不一致状态结束,这意味着提交了数据库事务,但永远不会发布领域事件。
In this case, the process running the logic for some reason fails to publish the domain events. Perhaps the message bus is down. Or the server running the code fails right after committing the database transaction, but before publishing the events the system will still end in an inconsistent state, which means that the database transaction is committed, but the domain events will never be published.
这些边缘情况可以使用发件箱模式来解决。
These edge cases can be addressed using the outbox pattern.
发件箱模式(图 9-11)确保可靠使用以下算法发布域事件:
The outbox pattern (Figure 9-11) ensures reliable publishing of domain events using the following algorithm:
更新后的聚合状态和新领域事件都在同一个原子事务中提交。
Both the updated aggregate’s state and the new domain events are committed in the same atomic transaction.
消息中继从数据库中获取新提交的域事件。
A message relay fetches newly committed domain events from the database.
中继将域事件发布到消息总线。
The relay publishes the domain events to the message bus.
成功发布后,中继要么将事件标记为published数据库中的事件,要么将其完全删除。
Upon successful publishing, the relay either marks the events as published in the database or deletes them completely.
使用关系数据库时,可以方便地利用数据库的能力以原子方式提交到两个表并使用专用表来存储消息,如图 9-12所示。
When using a relational database, it’s convenient to leverage the database’s ability to commit to two tables atomically and use a dedicated table for storing the messages, as shown in Figure 9-12.
当使用不支持多文档事务的 NoSQL 数据库时,传出域事件必须嵌入到聚合记录中。例如:
When using a NoSQL database that doesn’t support multidocument transactions, the outgoing domain events have to be embedded in the aggregate’s record. For example:
{"campaign-id":"364b33c3-2171-446d-b652-8e5a7b2be1af","state":{"name":"Autumn 2017","publishing-state":"DEACTIVATED","ad-locations":[...]...},"outbox":[{"campaign-id":"364b33c3-2171-446d-b652-8e5a7b2be1af","type":"campaign-deactivated","reason":"Goals met","published":false}]}
{"campaign-id":"364b33c3-2171-446d-b652-8e5a7b2be1af","state":{"name":"Autumn 2017","publishing-state":"DEACTIVATED","ad-locations":[...]...},"outbox":[{"campaign-id":"364b33c3-2171-446d-b652-8e5a7b2be1af","type":"campaign-deactivated","reason":"Goals met","published":false}]}
在此示例中,您可以看到 JSON 文档的附加属性 ,outbox其中包含必须发布的域事件列表。
In this sample, you can see the JSON document’s additional property, outbox, containing a list of domain events that have to be published.
发布中继可以以基于拉取或基于推送的方式获取新的域事件:
The publishing relay can fetch the new domain events in either a pull-based or push-based manner:
重要的是要注意发件箱模式保证消息至少传递一次:如果中继在发布消息后立即失败但在将其标记为已发布到数据库之前,则相同的消息将在下一次迭代中再次发布。
It’s important to note that the outbox pattern guarantees delivery of the messages at least once: if the relay fails right after publishing a message but before marking it as published in the database, the same message will be published again in the next iteration.
接下来,我们将了解如何利用域事件的可靠发布来克服聚合设计原则强加的一些限制。
Next, we’ll take a look at how we can leverage the reliable publishing of domain events to overcome some of the limitations imposed by aggregate design principles.
核心聚合设计原则之一是限制每笔交易到聚合的单个实例。这确保了聚合的边界被仔细考虑并封装了一组连贯的业务功能。但在某些情况下,您必须实施跨越多个聚合的业务流程。
One of the core aggregate design principles is to limit each transaction to a single instance of an aggregate. This ensures that an aggregate’s boundaries are carefully considered and encapsulate a coherent set of business functionality. But there are cases when you have to implement a business process that spans multiple aggregates.
考虑以下示例:当广告活动激活后,它应自动将活动的广告材料提交给其发布商。收到发布者的确认后,活动的发布状态应更改为Published。在发布商拒绝的情况下,活动应标记为Rejected。
Consider the following example: when an advertising campaign is activated, it should automatically submit the campaign’s advertising materials to its publisher. Upon receiving the confirmation from the publisher, the campaign’s publishing state should change to Published. In the case of rejection by the publisher, the campaign should be marked as Rejected.
此流程跨越两个业务实体:广告活动和发布商。将实体放在同一个聚合边界中肯定是矫枉过正,因为这些显然是不同的业务实体,具有不同的职责并且可能属于不同的限界上下文。相反,这个流程可以作为传奇来实现。
This flow spans two business entities: advertising campaign and publisher. Co-locating the entities in the same aggregate boundary would definitely be overkill, as these are clearly different business entities that have different responsibilities and may belong to different bounded contexts. Instead, this flow can be implemented as a saga.
saga 是一个长期运行的业务流程。长期运行不一定就时间而言,sagas 可以从几秒到几年不等,但就交易而言:跨越多个交易的业务流程。事务不仅可以由聚合处理,还可以由发出域事件和响应命令的任何组件处理。saga 监听相关组件发出的事件,并向其他组件发出后续命令。如果其中一个执行步骤失败,saga 负责发布相关的补偿动作以确保系统状态保持一致。
A saga is a long-running business process. It’s long running not necessarily in terms of time, as sagas can run from seconds to years, but rather in terms of transactions: a business process that spans multiple transactions. The transactions can be handled not only by aggregates but by any component emitting domain events and responding to commands. The saga listens to the events emitted by the relevant components and issues subsequent commands to the other components. If one of the execution steps fails, the saga is in charge of issuing relevant compensating actions to ensure the system state remains consistent.
让我们看看如何将前面示例中的广告活动发布流程实现为 saga,如图9-13所示。
Let’s see how the advertising campaign publishing flow from the preceding example can be implemented as a saga, as shown in Figure 9-13.
为了实现发布过程,saga 必须监听来自聚合的CampaignActivated事件Campaign以及PublishingConfirmed来自有界上下文的PublishingRejected事件AdPublishing。saga 必须SubmitAdvertisement在 上执行命令AdPublishing,在聚合上执行TrackPublishingConfirmation和命令。在此示例中,该命令充当补偿操作,以确保广告活动未被列为有效。这是代码:TrackPublishingRejectionCampaignTrackPublishingRejection
To implement the publishing process, the saga has to listen to the CampaignActivated event from the Campaign aggregate and the PublishingConfirmed and PublishingRejected events from the AdPublishing bounded context. The saga has to execute the SubmitAdvertisement command on AdPublishing, and the TrackPublishingConfirmation and TrackPublishingRejection commands on the Campaign aggregate. In this example, the TrackPublishingRejection command acts as a compensation action that will ensure that the advertising campaign is not listed as active. Here is the code:
publicclassCampaignPublishingSaga{privatereadonlyICampaignRepository_repository;privatereadonlyIPublishingServiceClient_publishingService;...publicvoidProcess(CampaignActivated@event){varcampaign=_repository.Load(@event.CampaignId);varadvertisingMaterials=campaign.GenerateAdvertisingMaterials();_publishingService.SubmitAdvertisement(@event.CampaignId,advertisingMaterials);}publicvoidProcess(PublishingConfirmed@event){varcampaign=_repository.Load(@event.CampaignId);campaign.TrackPublishingConfirmation(@event.ConfirmationId);_repository.CommitChanges(campaign);}publicvoidProcess(PublishingRejected@event){varcampaign=_repository.Load(@event.CampaignId);campaign.TrackPublishingRejection(@event.RejectionReason);_repository.CommitChanges(campaign);}}
publicclassCampaignPublishingSaga{privatereadonlyICampaignRepository_repository;privatereadonlyIPublishingServiceClient_publishingService;...publicvoidProcess(CampaignActivated@event){varcampaign=_repository.Load(@event.CampaignId);varadvertisingMaterials=campaign.GenerateAdvertisingMaterials();_publishingService.SubmitAdvertisement(@event.CampaignId,advertisingMaterials);}publicvoidProcess(PublishingConfirmed@event){varcampaign=_repository.Load(@event.CampaignId);campaign.TrackPublishingConfirmation(@event.ConfirmationId);_repository.CommitChanges(campaign);}publicvoidProcess(PublishingRejected@event){varcampaign=_repository.Load(@event.CampaignId);campaign.TrackPublishingRejection(@event.RejectionReason);_repository.CommitChanges(campaign);}}
前面的示例依赖于消息传递基础结构来传递相关事件,并通过执行相关命令来对事件做出反应。这是一个相对简单的传奇的例子:它没有状态。你会遇到确实需要状态管理的传奇;例如,跟踪已执行的操作,以便在发生故障时可以发布相关的补偿操作。在这种情况下,saga 可以作为事件源聚合来实现,保存接收到的事件和发出的命令的完整历史记录。但是,命令执行逻辑应该从 saga 本身中移出并异步执行,类似于发件箱模式中域事件的调度方式:
The preceding example relies on the messaging infrastructure to deliver the relevant events, and it reacts to the events by executing the relevant commands. This is an example of a relatively simple saga: it has no state. You will encounter sagas that do require state management; for example, to track the executed operations so that relevant compensating actions can be issued in case of a failure. In such a situation, the saga can be implemented as an event-sourced aggregate, persisting the complete history of received events and issued commands. However, the command execution logic should be moved out of the saga itself and executed asynchronously, similar to the way domain events are dispatched in the outbox pattern:
publicclassCampaignPublishingSaga{privatereadonlyICampaignRepository_repository;privatereadonlyIList<IDomainEvent>_events;...publicvoidProcess(CampaignActivatedactivated){varcampaign=_repository.Load(activated.CampaignId);varadvertisingMaterials=campaign.GenerateAdvertisingMaterials();varcommandIssuedEvent=newCommandIssuedEvent(target:Target.PublishingService,command:newSubmitAdvertisementCommand(activated.CampaignId,advertisingMaterials));_events.Append(activated);_events.Append(commandIssuedEvent);}publicvoidProcess(PublishingConfirmedconfirmed){varcommandIssuedEvent=newCommandIssuedEvent(target:Target.CampaignAggregate,command:newTrackConfirmation(confirmed.CampaignId,confirmed.ConfirmationId));_events.Append(confirmed);_events.Append(commandIssuedEvent);}publicvoidProcess(PublishingRejectedrejected){varcommandIssuedEvent=newCommandIssuedEvent(target:Target.CampaignAggregate,command:newTrackRejection(rejected.CampaignId,rejected.RejectionReason));_events.Append(rejected);_events.Append(commandIssuedEvent);}}
publicclassCampaignPublishingSaga{privatereadonlyICampaignRepository_repository;privatereadonlyIList<IDomainEvent>_events;...publicvoidProcess(CampaignActivatedactivated){varcampaign=_repository.Load(activated.CampaignId);varadvertisingMaterials=campaign.GenerateAdvertisingMaterials();varcommandIssuedEvent=newCommandIssuedEvent(target:Target.PublishingService,command:newSubmitAdvertisementCommand(activated.CampaignId,advertisingMaterials));_events.Append(activated);_events.Append(commandIssuedEvent);}publicvoidProcess(PublishingConfirmedconfirmed){varcommandIssuedEvent=newCommandIssuedEvent(target:Target.CampaignAggregate,command:newTrackConfirmation(confirmed.CampaignId,confirmed.ConfirmationId));_events.Append(confirmed);_events.Append(commandIssuedEvent);}publicvoidProcess(PublishingRejectedrejected){varcommandIssuedEvent=newCommandIssuedEvent(target:Target.CampaignAggregate,command:newTrackRejection(rejected.CampaignId,rejected.RejectionReason));_events.Append(rejected);_events.Append(commandIssuedEvent);}}
在此示例中,发件箱中继必须针对每个实例在相关端点上执行命令CommandIssuedEvent。与发布领域事件的情况一样,将 saga 的状态转换与命令的执行分开确保命令将可靠地执行,即使过程在任何阶段失败也是如此。
In this example, the outbox relay will have to execute the commands on relevant endpoints for each instance of CommandIssuedEvent. As in the case of publishing domain events, separating the transition of the saga’s state from the execution of commands ensures that the commands will be executed reliably, even if the process fails at any stage.
尽管 saga 模式编排了一个多组件事务,所涉及组件的状态最终是一致的。虽然 saga 最终会执行相关命令,但没有两个事务可以被认为是原子的。这与另一个聚合设计原则相关:
Although the saga pattern orchestrates a multicomponent transaction, the states of the involved components are eventually consistent. And although the saga will eventually execute the relevant commands, no two transactions can be considered atomic. This correlates with another aggregate design principle:
只有聚合边界内的数据才能被认为是强一致的。外面的一切最终都是一致的。
Only the data within an aggregate’s boundaries can be considered strongly consistent. Everything outside is eventually consistent.
使用此作为指导原则,以确保您没有滥用 sagas 来补偿不正确的聚合边界。必须属于同一聚合的业务操作需要高度一致的数据。
Use this as a guiding principle to make sure you are not abusing sagas to compensate for improper aggregate boundaries. Business operations that have to belong to the same aggregate require strongly consistent data.
传奇模式经常与另一种模式混淆:流程管理器。尽管实现方式相似,但这些是不同的模式。在下一节中,我们将讨论流程管理器模式的目的以及它与传奇模式的区别。
The saga pattern is often confused with another pattern: process manager. Although the implementation is similar, these are different patterns. In the next section, we’ll discuss the purpose of the process manager pattern and how it differs from the saga pattern.
saga 模式管理简单的线性流。严格说起来,saga 将事件与相应的命令相匹配。在我们用来演示 saga 实现的示例中,我们实际上实现了事件与命令的简单匹配:
The saga pattern manages simple, linear flow. Strictly speaking, a saga matches events to the corresponding commands. In the examples we used to demonstrate saga implementations, we actually implemented simple matching of events to commands:
CampaignActivated事件PublishingService.SubmitAdvertisement 命令
CampaignActivated event to PublishingService.SubmitAdvertisement command
PublishingConfirmed事件Campaign.TrackConfirmation命令
PublishingConfirmed event to Campaign.TrackConfirmation command
PublishingRejected事件Campaign.TrackRejection命令
PublishingRejected event to Campaign.TrackRejection command
如图 9-14所示,流程管理器模式旨在实现基于业务逻辑的流程。它被定义为一个中央处理单元,它维护序列的状态并确定接下来的处理步骤。2个
The process manager pattern, shown in Figure 9-14, is intended to implement a business-logic-based process. It is defined as a central processing unit that maintains the state of the sequence and determines the next processing steps.2
作为一个简单的经验法则,如果一个 saga 包含 if-else 语句来选择正确的操作过程,它可能是一个流程管理器。
As a simple rule of thumb, if a saga contains if-else statements to choose the correct course of action, it is probably a process manager.
流程管理器和 saga 之间的另一个区别是 saga 是在观察到特定事件时隐式实例化的,如CampaignActivated前面的示例所示。另一方面,流程管理器不能绑定到单个源事件。相反,它是一个由多个步骤组成的连贯业务流程。因此,必须显式实例化流程管理器。考虑以下示例:
Another difference between a process manager and a saga is that a saga is instantiated implicitly when a particular event is observed, as in CampaignActivated in the preceding examples. A process manager, on the other hand, cannot be bound to a single source event. Instead, it’s a coherent business process consisting of multiple steps. Hence, a process manager has to be instantiated explicitly. Consider the following example:
预订商务旅行从路由算法选择最具成本效益的飞行路线并要求员工批准开始。如果员工喜欢不同的路线,他们的直接经理需要批准。预订航班后,必须在适当的日期预订预先批准的酒店之一。如果没有可用的酒店,则必须取消机票。
Booking a business trip starts with the routing algorithm choosing the most cost-effective flight route and asking the employee to approve it. In case the employee prefers a different route, their direct manager needs to approve it. After the flight is booked, one of the preapproved hotels has to be booked for the appropriate dates. If no hotels are available, the flight tickets have to be canceled.
在这个例子中,没有中央实体来触发旅行预订过程。行程预订是流程,必须作为流程管理器来实现(见图9-15)。
In this example, there is no central entity to trigger the trip booking process. The trip booking is the process and it has to be implemented as a process manager (see Figure 9-15).
从实现的角度来看,流程管理器通常被实现为基于状态或事件源的聚合。例如:
From an implementation perspective, process managers are often implemented as aggregates, either state based or event sourced. For example:
publicclassBookingProcessManager{privatereadonlyIList<IDomainEvent>_events;privateBookingId_id;privateDestination_destination;privateTripDefinition_parameters;privateEmployeeId_traveler;privateRoute_route;privateIList<Route>_rejectedRoutes;privateIRoutingService_routing;...publicvoidInitialize(Destinationdestination,TripDefinitionparameters,EmployeeIdtraveler){_destination=destination;_parameters=parameters;_traveler=traveler;_route=_routing.Calculate(destination,parameters);varrouteGenerated=newRouteGeneratedEvent(BookingId:_id,Route:_route);varcommandIssuedEvent=newCommandIssuedEvent(command:newRequestEmployeeApproval(_traveler,_route));_events.Append(routeGenerated);_events.Append(commandIssuedEvent);}publicvoidProcess(RouteConfirmedconfirmed){varcommandIssuedEvent=newCommandIssuedEvent(command:newBookFlights(_route,_parameters));_events.Append(confirmed);_events.Append(commandIssuedEvent);}publicvoidProcess(RouteRejectedrejected){varcommandIssuedEvent=newCommandIssuedEvent(command:newRequestRerouting(_traveler,_route));_events.Append(rejected);_events.Append(commandIssuedEvent);}publicvoidProcess(ReroutingConfirmedconfirmed){_rejectedRoutes.Append(route);_route=_routing.CalculateAltRoute(destination,parameters,rejectedRoutes);varrouteGenerated=newRouteGeneratedEvent(BookingId:_id,Route:_route);varcommandIssuedEvent=newCommandIssuedEvent(command:newRequestEmployeeApproval(_traveler,_route));_events.Append(confirmed);_events.Append(routeGenerated);_events.Append(commandIssuedEvent);}publicvoidProcess(FlightBookedbooked){varcommandIssuedEvent=newCommandIssuedEvent(command:newBookHotel(_destination,_parameters));_events.Append(booked);_events.Append(commandIssuedEvent);}...}
publicclassBookingProcessManager{privatereadonlyIList<IDomainEvent>_events;privateBookingId_id;privateDestination_destination;privateTripDefinition_parameters;privateEmployeeId_traveler;privateRoute_route;privateIList<Route>_rejectedRoutes;privateIRoutingService_routing;...publicvoidInitialize(Destinationdestination,TripDefinitionparameters,EmployeeIdtraveler){_destination=destination;_parameters=parameters;_traveler=traveler;_route=_routing.Calculate(destination,parameters);varrouteGenerated=newRouteGeneratedEvent(BookingId:_id,Route:_route);varcommandIssuedEvent=newCommandIssuedEvent(command:newRequestEmployeeApproval(_traveler,_route));_events.Append(routeGenerated);_events.Append(commandIssuedEvent);}publicvoidProcess(RouteConfirmedconfirmed){varcommandIssuedEvent=newCommandIssuedEvent(command:newBookFlights(_route,_parameters));_events.Append(confirmed);_events.Append(commandIssuedEvent);}publicvoidProcess(RouteRejectedrejected){varcommandIssuedEvent=newCommandIssuedEvent(command:newRequestRerouting(_traveler,_route));_events.Append(rejected);_events.Append(commandIssuedEvent);}publicvoidProcess(ReroutingConfirmedconfirmed){_rejectedRoutes.Append(route);_route=_routing.CalculateAltRoute(destination,parameters,rejectedRoutes);varrouteGenerated=newRouteGeneratedEvent(BookingId:_id,Route:_route);varcommandIssuedEvent=newCommandIssuedEvent(command:newRequestEmployeeApproval(_traveler,_route));_events.Append(confirmed);_events.Append(routeGenerated);_events.Append(commandIssuedEvent);}publicvoidProcess(FlightBookedbooked){varcommandIssuedEvent=newCommandIssuedEvent(command:newBookHotel(_destination,_parameters));_events.Append(booked);_events.Append(commandIssuedEvent);}...}
在此示例中,流程管理器具有其显式 ID 和持久状态,描述必须预订的行程。与前面的 saga 模式示例一样,流程管理器订阅控制工作流的事件(RouteConfirmed、RouteRejected、ReroutingConfirmed等),并实例化CommandIssuedEvent将由发件箱中继处理以执行实际命令的类型的事件。
In this example, the process manager has its explicit ID and persistent state, describing the trip that has to be booked. As in the earlier example of a saga pattern, the process manager subscribes to events that control the workflow (RouteConfirmed, RouteRejected, ReroutingConfirmed, etc.), and it instantiates events of type CommandIssuedEvent that will be processed by an outbox relay to execute the actual commands.
在本章中,您了解了集成系统组件的不同模式。本章首先探索可用于实现反腐败层或开放主机服务的模型转换模式。我们看到翻译可以即时处理,也可以遵循更复杂的逻辑,需要状态跟踪。
In this chapter, you learned the different patterns for integrating a system’s components. The chapter began by exploring patterns for model translations that can be used to implement anticorruption layers or open-host services. We saw that translations can be handled on the fly or can follow a more complex logic, requiring state tracking.
发件箱模式是发布聚合领域事件的可靠方式。它确保领域事件始终会被发布,即使面对不同的流程失败。
The outbox pattern is a reliable way to publish aggregates’ domain events. It ensures that domain events are always going to be published, even in the face of different process failures.
saga 模式可用于实现简单的跨组件业务流程。使用流程管理器模式可以实现更复杂的业务流程。这两种模式都依赖于对域事件的异步反应和命令的发出。
The saga pattern can be used to implement simple cross-component business processes. More complex business processes can be implemented using the process manager pattern. Both patterns rely on asynchronous reactions to domain events and the issuing of commands.
哪种限界上下文集成模式需要实现模型转换逻辑?
墨守成规
反腐层
开放主机服务
乙丙
Which bounded context integration pattern requires implementation of model transformation logic?
Conformist
Anticorruption layer
Open-host service
B and C
发件箱模式的目标是什么?
将消息传递基础架构与系统的业务逻辑层分离
可靠地发布消息
支持事件源领域模型模式的实现
甲乙丙
What is the goal of the outbox pattern?
Decouple messaging infrastructure from the system’s business logic layer
Reliably publish messages
Support implementation of the event-sourced domain model pattern
A and C
除了将消息发布到消息总线之外,发件箱模式还有哪些其他可能的用例?
Apart from publishing messages to a message bus, what are other possible use cases for the outbox pattern?
传奇和流程管理器模式之间有什么区别?
流程管理器需要显式实例化,而传奇是在发布相关领域事件时执行的。
与流程管理器相反,传奇从不需要持久化其执行状态。
saga 需要它操作的组件来实现事件溯源模式,而流程管理器则不需要。
流程管理器模式适用于复杂的业务工作流。
A和D是正确的。
What are the differences between the saga and process manager patterns?
A process manager requires explicit instantiation, while a saga is executed when a relevant domain event is published.
Contrary to a process manager, a saga never requires persistence of its execution state.
A saga requires the components it manipulates to implement the event sourcing pattern, while a process manager doesn’t.
The process manager pattern is suitable for complex business workflows.
A and D are correct.
1个理查森 C.(2019 年)。微服务模式:带有 Java 示例。纽约:曼宁出版社。
1 Richardson, C. (2019). Microservice Patterns: With Examples in Java. New York: Manning Publications.
2个Hohpe, G., & Woolf, B. (2003)。企业集成模式:设计、构建和部署消息传递解决方案。波士顿:Addison-Wesley。
2 Hohpe, G., & Woolf, B. (2003). Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Boston: Addison-Wesley.
在第一部分和第二部分中,我们讨论了用于制定战略和战术设计决策的领域驱动设计工具。在本书的这一部分,我们从理论走向实践。您将学习在实际项目中应用领域驱动设计。
In Parts I and II, we discussed domain-driven design tools for making strategic and tactical design decisions. In this part of the book, we move from theory to practice. You will learn to apply domain-driven design in real-life projects.
第 10 章将我们讨论的战略和战术设计合并为简单的经验法则,简化了设计决策的过程。您将学会快速识别与业务领域的复杂性和需求相匹配的模式。
Chapter 10 merges what we discussed about strategic and tactical design into simple rules of thumb that streamline the process of making design decisions. You will learn to quickly identify patterns that match the business domain’s complexity and needs.
在第 11 章中,我们将从不同的角度审视领域驱动设计。设计出色的解决方案很重要,但还不够。随着项目的发展,我们必须保持它的形状。在本章中,您将学习应用领域驱动设计工具来随着时间的推移维护和改进软件设计决策。
In Chapter 11, we will look at domain-driven design from a different perspective. Designing a great solution is important, but not enough. We have to keep it in shape as the project evolves through time. In this chapter, you will learn to apply domain-driven design tools to maintain and evolve software design decisions over time.
第 12 章介绍 EventStorming:一种实践活动,可简化发现领域知识和构建通用语言的过程。
Chapter 12 introduces EventStorming: a hands-on activity that streamlines the process of discovering domain knowledge and building a ubiquitous language.
第 13 章总结了第 III 部分,其中选择了一些提示和技巧,用于“温和地”在棕地项目(我们最常从事的项目类型)中引入和合并领域驱动设计模式和实践。
Chapter 13 concludes Part III with a selection of tips and tricks for “gently” introducing and incorporating domain-driven design patterns and practices in brownfield projects—the kinds of projects we work on the most.
“这取决于”是软件工程中几乎所有问题的正确答案,但并不实用。在本章中,我们将探讨“它”依赖于什么。
“It depends” is the correct answer to almost any question in software engineering, but not really practical. In this chapter, we will explore what “it” depends on.
在本书的第一部分中,您学习了用于分析业务领域和制定战略设计决策的领域驱动设计工具。在第 II 部分中,我们探讨了战术设计模式:实现业务逻辑、组织系统架构以及在系统组件之间建立通信的不同方式。本章连接第一部分和第二部分。您将学习应用分析工具驱动各种软件设计决策的启发式方法:即(业务)领域驱动(软件)设计。
In Part I of the book, you learned domain-driven design tools for analyzing business domains and making strategic design decisions. In Part II, we explored tactical design patterns: the different ways to implement business logic, organize system architecture, and establish communication between a system’s components. This chapter bridges Parts I and II. You will learn heuristics for applying analysis tools to drive various software design decisions: that is, (business) domain-driven (software) design.
但首先,因为本章是关于设计启发式的,所以让我们从定义启发式这个术语开始。
But first, since this chapter is about design heuristics, let’s start by defining the term heuristic.
启发式不是一个有保证的和数学上的硬性规则证明在 100% 的情况下是正确的。相反,这是一个经验法则:不能保证完美,但足以满足一个人的近期目标。换句话说,使用启发法是一种有效的解决问题的方法,它忽略了许多线索中固有的噪音,而是关注最重要线索中反映的“沼泽力量”。1个
A heuristic is not a hard rule that is guaranteed and mathematically proven to be correct in 100% of cases. Rather, it’s a rule of thumb: not guaranteed to be perfect, yet sufficient for one’s immediate goals. In other words, using heuristics is an effective problem-solving approach that ignores the noise inherent in many cues, focusing instead on the “swamping forces” reflected in the most important cues.1
本章介绍的启发式方法侧重于不同业务领域的基本属性以及各种设计决策所解决的问题的本质。
The heuristics presented in this chapter focus on the essential properties of the different business domains and on the essence of the problems addressed by the various design decisions.
正如您在第 3 章中回忆的那样,两者宽边界和窄边界可以符合包含一致的无处不在语言的有效边界上下文的定义。但是,有界上下文的最佳大小是多少?鉴于限界上下文与微服务的频繁等式,这个问题尤为重要。2个
As you’ll recall from Chapter 3, both wide and narrow boundaries could fit the definition of a valid bounded context encompassing a consistent ubiquitous language. But still, what is the optimal size of a bounded context? This question is especially important in light of the frequent equation of bounded contexts with microservices.2
我们是否应该始终争取尽可能小的有界上下文?正如我的朋友 Nick Tune 所说:
Should we always strive for the smallest possible bounded contexts? As my friend Nick Tune says:
有许多有用的启发式方法可用于定义服务的边界。尺寸是最没用的之一。
There are many useful and revealing heuristics for defining the boundaries of a service. Size is one of the least useful.
与其让模型成为所需大小的函数(针对小的有界上下文进行优化),不如做相反的事情更有效:将有界上下文的大小视为它包含的模型的函数。
Rather than making the model a function of the desired size—optimizing for small bounded contexts—it’s much more effective to do the opposite: treat the bounded context’s size as a function of the model it encompasses.
影响多个限界上下文的软件更改代价高昂,并且需要大量协调,尤其是当受影响的限界上下文由不同团队实施时。未封装在单个有界上下文中的此类更改表明上下文边界的设计效率低下。不幸的是,重构有界上下文边界是一项代价高昂的工作,而且在许多情况下,无效的边界仍然无人看管,最终会累积技术债务(见图10-1)。
Software changes affecting multiple bounded contexts are expensive and require lots of coordination, especially if the affected bounded contexts are implemented by different teams. Such changes that are not encapsulated in a single bounded context signal ineffective design of the contexts’ boundaries. Unfortunately, refactoring bounded context boundaries is an expensive undertaking, and in many cases, the ineffective boundaries remain unattended and end up accumulating technical debt (see Figure 10-1).
使限界上下文边界无效的更改通常发生在业务领域不广为人知或业务需求频繁更改时。正如您在第 1 章中了解到的,波动性和不确定性都是核心子域的属性,尤其是在实施的早期阶段。我们可以将其用作设计有界上下文边界的启发式方法。
Changes that invalidate the bounded contexts’ boundaries typically occur when the business domain is not well known or the business requirements change frequently. As you learned in Chapter 1, both volatility and uncertainty are the properties of core subdomains, especially at the early stages of implementation. We can use it as a heuristic for designing bounded context boundaries.
广泛的有界上下文边界,或那些包含多个子域的边界,使得在包含子域的边界或模型上出错更安全。重构逻辑边界比重构物理边界要便宜得多。因此,在设计有界上下文时,从更宽的边界开始。如果需要,在获得领域知识时将宽边界分解为更小的边界。
Broad bounded context boundaries, or those that encompass multiple subdomains, make it safer to be wrong about the boundaries or the models of the included subdomains. Refactoring logical boundaries is considerably less expensive than refactoring physical boundaries. Hence, when designing bounded contexts, start with wider boundaries. If required, decompose the wide boundaries into smaller ones as you gain domain knowledge.
这种启发式主要适用于包含核心子域的有界上下文,因为通用子域和支持子域都更加公式化并且更不易变。创建包含核心子域的限界上下文时,您可以通过包含核心子域最常与之交互的其他子域来保护自己免受不可预见的更改。这可以是其他核心子域,甚至可以是支持子域和通用子域,如图10-2所示。
This heuristic applies mainly to bounded contexts encompassing core subdomains, as both generic and supporting subdomains are more formularized and much less volatile. When creating a bounded context that contains a core subdomain, you can protect yourself against unforeseen changes by including other subdomains that the core subdomain interacts with most often. This can be other core subdomains, or even supporting and generic subdomains, as shown in Figure 10-2.
在第 5 – 7章中,我们讨论了通过详细介绍业务逻辑,您学习了四种不同的业务逻辑建模方法:事务脚本、活动记录、域模型和事件源域模型模式。
In Chapters 5–7, where we discussed business logic in detail, you learned four different ways to model business logic: the transaction script, active record, domain model, and event-sourced domain model patterns.
事务脚本和活动记录模式都更好适用于具有简单业务逻辑的子域:例如,支持子域或为通用子域集成第三方解决方案。两种模式之间的区别在于数据结构的复杂性。事务脚本模式可用于简单的数据结构,而活动记录模式有助于封装复杂数据结构到底层数据库的映射。
Both the transaction script and active record patterns are better suited for subdomains with simple business logic: supporting subdomains or integrating a third-party solution for a generic subdomain, for example. The difference between the two patterns is the complexity of the data structures. The transaction script pattern can be used for simple data structures, while the active record pattern helps to encapsulate the mapping of complex data structures to the underlying database.
领域模型及其变体,事件源领域模型,适用于具有复杂业务逻辑的子域:核心子域。处理货币交易的核心子域,根据法律有义务提供审计日志,或者需要对系统行为进行深入分析,事件源域模型可以更好地解决这些问题。
The domain model and its variant, the event-sourced domain model, lend themselves to subdomains that have complex business logic: core subdomains. Core subdomains that deal with monetary transactions, are obligated by law to provide an audit log, or require deep analytics of the system’s behavior are better addressed by the event-sourced domain model.
考虑到所有这些,选择合适的业务逻辑实现模式的有效启发式方法是提出以下问题:
With all of this in mind, an effective heuristic for choosing the appropriate business logic implementation pattern is to ask the following questions:
子域是否跟踪金钱或其他货币交易或是否必须提供一致的审计日志,或者业务是否需要对其行为进行深入分析?如果是这样,请使用事件源域模型。否则...
Does the subdomain track money or other monetary transactions or have to provide a consistent audit log, or is deep analysis of its behavior required by the business? If so, use the event-sourced domain model. Otherwise...
子域的业务逻辑复杂吗?如果是这样,请实施领域模型。否则...
Is the subdomain’s business logic complex? If so, implement a domain model. Otherwise...
子域是否包含复杂的数据结构?如果是这样,请使用活动记录模式。否则...
Does the subdomain include complex data structures? If so, use the active record pattern. Otherwise...
实现交易脚本。
Implement a transaction script.
由于子域的复杂性和类型之间存在很强的关系,我们可以使用域驱动的决策树来可视化启发式方法,如图10-3所示。
Since there is a strong relationship between a subdomain’s complexity and its type, we can visualize the heuristics using a domain-driven decision tree, as shown in Figure 10-3.
我们可以使用另一种启发式来定义复杂和简单业务逻辑之间的区别。这两种类型的业务逻辑之间的界限不是很明显,但是很有用。一般来说,复杂的业务逻辑包括复杂的业务规则、不变量和算法。一种简单的方法主要围绕验证输入。评估复杂性的另一种启发式方法涉及无处不在的语言本身的复杂性。主要是描述CRUD操作,还是描述更复杂的业务流程和规则?
We can use another heuristic to define the difference between complex and simple business logic. The line between these two types of business logic is not terribly sharp, but it’s useful. In general, complex business logic includes complicated business rules, invariants, and algorithms. A simple approach mainly revolves around validating the inputs. Another heuristic for evaluating complexity concerns the complexity of the ubiquitous language itself. Is it mainly describing CRUD operations, or is it describing more complicated business processes and rules?
根据业务逻辑及其数据结构的复杂性来确定业务逻辑实现模式是验证您对子域类型的假设的一种方式。假设您认为它是一个核心子域,但最好的模式是活动记录或事务脚本。或者假设您认为支持子域需要域模型或事件源域模型;在这种情况下,这是一个很好的机会来重新审视您对子域和业务域的一般假设。请记住,核心子域的竞争优势不一定是技术性的。
Deciding on the business logic implementation pattern according to the complexity of the business logic and its data structures is a way to validate your assumptions about the subdomain type. Suppose you consider it to be a core subdomain, but the best pattern is active record or transaction script. Or suppose what you believe is a supporting subdomain requires a domain model or an event-sourced domain model; in this case, it’s an excellent opportunity to revisit your assumptions about the subdomain and business domain in general. Remember, a core subdomain’s competitive advantage is not necessarily technical.
在第 8 章中,您学习了关于三种架构模式:分层架构、端口和适配器以及 CQRS。
In Chapter 8, you learned about the three architectural patterns: layered architecture, ports & adapters, and CQRS.
了解预期的业务逻辑实现模式可以直接选择架构模式:
Knowing the intended business logic implementation pattern makes choosing an architectural pattern straightforward:
The event-sourced domain model requires CQRS. Otherwise, the system will be extremely limited in its data querying options, fetching a single instance by its ID only.
The domain model requires the ports & adapters architecture. Otherwise, the layered architecture makes it hard to make aggregates and value objects ignorant of persistence.
The Active record pattern is best accompanied by a layered architecture with the additional application (service) layer. This is for the logic controlling the active records.
The transaction script pattern can be implemented with a minimal layered architecture, consisting of only three layers.
上述启发式方法的唯一例外是 CQRS 模式。CQRS 不仅有利于事件源域模型,而且如果子域需要在多个持久模型中表示其数据,则也有利于任何其他模式。
The only exception to the preceding heuristics is the CQRS pattern. CQRS can be beneficial not only for the event-sourced domain model, but also for any other pattern if the subdomain requires representing its data in multiple persistent models.
图 10-4显示了根据这些启发式方法选择架构模式的决策树。
Figure 10-4 shows a decision tree for choosing an architectural pattern based on these heuristics.
业务逻辑实现的知识模式和架构模式可以用作为代码库选择测试策略的启发式方法。看看图 10-5中显示的三种测试策略。
The knowledge of both the business logic implementation pattern and the architectural pattern can be used as a heuristic for choosing a testing strategy for the codebase. Take a look at the three testing strategies shown in Figure 10-5.
图中测试策略之间的区别在于它们强调不同类型的测试:单元、集成和端到端。让我们分析每种策略以及应使用每种模式的上下文。
The difference between the testing strategies in the figure is their emphasis on the different types of tests: unit, integration, and end-to-end. Let’s analyze each strategy and the context in which each pattern should be used.
经典的测试金字塔强调单元测试,较少的集成测试,甚至更少的端到端测试。域模型模式的两种变体都最好用测试金字塔来解决。聚合和值对象是有效测试业务逻辑的完美单元。
The classic testing pyramid emphasizes unit tests, fewer integration tests, and even fewer end-to-end tests. Both variants of the domain model patterns are best addressed with the testing pyramid. Aggregates and value objects make perfect units for effectively testing the business logic.
测试钻石最关注集成测试。当使用活动记录模式时,根据定义,系统的业务逻辑分布在服务层和业务逻辑层。因此,着眼于整合两层,测试金字塔是更有效的选择。
The testing diamond focuses the most on integration tests. When the active record pattern is used, the system’s business logic is, by definition, spread across both the service and business logic layers. Therefore, to focus on integrating the two layers, the testing pyramid is the more effective choice.
反向测试金字塔最关注端到端测试:从头到尾验证应用程序的工作流程。这种方式最适合实现事务脚本模式的代码库:业务逻辑简单,层数最少,更有效地验证系统端到端的流程。
The reversed testing pyramid attributes the most attention to end-to-end tests: verifying the application’s workflow from beginning to end. Such an approach best fits codebases implementing the transaction script pattern: the business logic is simple and the number of layers is minimal, making it more effective to verify the end-to-end flow of the system.
图 10-6显示了测试策略决策树。
Figure 10-6 shows the testing strategy decision tree.
业务逻辑模式、架构模式和测试策略启发式可以用战术设计决策树进行统一和总结,如图10-7所示。
The business logic patterns, architectural patterns, and testing strategy heuristics can be unified and summarized with a tactical design decision tree, as depicted in Figure 10-7.
如您所见,识别子域类型并遵循决策树为您提供了一个坚实的起点来做出基本的设计决策。也就是说,重要的是要重申这些是启发式的,而不是硬性规定。每条规则都有例外,更不用说启发式了,根据定义,这些规则并不是在 100% 的情况下都是正确的。
As you can see, identifying subdomains types and following the decision tree gives you a solid starting point for making the essential design decisions. That said, it’s important to reiterate that these are heuristics, not hard rules. There is an exception to every rule, let alone heuristics, that by definition are not intended to be correct in 100% of the cases.
决策树基于我对使用简单工具的偏好,并且仅在绝对必要时才采用高级模式——域模型、事件源域模型、CQRS等等。另一方面,我遇到过在实施事件源域模型方面具有丰富经验的团队,因此将其用于所有子域。对于他们来说,这比使用不同的模式更简单。我可以向所有人推荐这种方法吗?当然不是。在我工作过或咨询过的公司中,基于启发式的方法比对每个问题都使用相同的解决方案更有效。
The decision tree is based on my preference to use the simple tools, and resort to the advanced patterns—domain model, event-sourced domain model, CQRS, and so on—only when absolutely necessary. On the other hand, I’ve met teams that have a lot of experience implementing the event-sourced domain model and therefore use it for all their subdomains. For them it’s simpler than using different patterns. Can I recommend this approach to everyone? Of course not. In the companies I have worked for or consulted, the heuristics-based approach was more efficient than using the same solution for every problem.
归根结底,这取决于您的具体情况。使用图 10-7中所示的决策树及其所基于的设计启发式作为指导原则,但不能替代批判性思维。如果您发现替代启发式方法更适合您,请随时更改指导原则或完全构建您自己的决策树。
At the end of the day, it depends on your specific context. Use the decision tree illustrated in Figure 10-7, and the design heuristics it is based on, as guiding principles, but not as a replacement for critical thinking. If you find that alternative heuristics fit you better, feel free to alter the guiding principles or build your own decision tree altogether.
本章将本书的第一部分和第二部分与基于启发式的决策框架联系起来。您学习了如何应用业务领域及其子领域的知识来推动技术决策:选择安全的有界上下文边界、为应用程序的业务逻辑建模以及确定编排每个有界上下文内部组件交互所需的架构模式。最后,我们绕道而行,进入一个经常引起激烈争论的不同话题——什么样的测试更重要——并使用相同的框架根据业务领域对不同的测试进行优先级排序。
This chapter connected Parts I and II of the book to a heuristic-based decision framework. You learned how to apply the knowledge of the business domain and its subdomains to drive technical decisions: choosing safe bounded context boundaries, modeling the application’s business logic, and determining the architectural pattern needed to orchestrate the interactions of each bounded context’s internal components. Finally, we took a detour into a different topic that is often a subject of passionate arguments—what kind of test is more important—and used the same framework to prioritize the different tests according to the business domain.
做出设计决策很重要,但随着时间的推移验证决策的有效性更为重要。在下一章中,我们将把讨论转移到软件设计生命周期的下一阶段:设计决策的演变。
Making design decisions is important, but even more so is to verify the decisions’ validity over time. In the next chapter, we will shift our discussion to the next phase of the software design lifecycle: the evolution of design decisions.
假设您正在实施 WolfDesk 的(请参阅前言)票证生命周期管理系统。它是一个核心子域,需要对其行为进行深入分析,以便随着时间的推移进一步优化算法。您实施业务逻辑和组件架构的初始策略是什么?你的测试策略是什么?
Assume you are implementing WolfDesk’s (see Preface) ticket lifecycle management system. It’s a core subdomain that requires deep analysis of its behavior so that the algorithm can be further optimized over time. What would be your initial strategy implementing the business logic and the component’s architecture? What would be your testing strategy?
对于 WolfDesk 的支持代理的轮班管理模块,您的设计决策是什么?
What would be your design decisions for WolfDesk’s support agents’ shift management module?
为了简化管理代理轮班的过程,您希望为不同的地理区域使用公共假期的外部提供者。该过程通过定期调用外部提供者并获取即将到来的公共假期的日期和名称来工作。您将使用哪些业务逻辑和架构模式来实施集成?你会如何测试它?
To ease the process of managing agents’ shifts, you want to use an external provider of public holidays for different geographical regions. The process works by periodically calling the external provider and fetching the dates and names of forthcoming public holidays. What business logic and architectural patterns would you use to implement the integration? How would you test it?
根据您的经验,软件开发过程的哪些其他方面可以包含在本章介绍的基于启发式的决策树中?
Based on your experience, what other aspects of the software development process can be included in the heuristics-based decision tree presented in this chapter?
1个Gigerenzer, G.、Todd, PM 和 ABC 研究组(研究组,马克斯普朗克研究所,德国)。(1999)。使我们聪明的简单启发法。纽约:牛津大学出版社。
1 Gigerenzer, G., Todd, P. M., & ABC Research Group (Research Group, Max Planck Institute, Germany). (1999). Simple Heuristics That Make Us Smart. New York: Oxford University Press.
2个 第 11 章专门介绍限界上下文和微服务之间的相互作用。
2 Chapter 11 is dedicated to the interplay between bounded contexts and microservices.
在我们生活的现代、快节奏的世界中,公司不能昏昏欲睡。为了跟上竞争的步伐,他们必须随着时间的推移不断改变、发展甚至重塑自己。我们在设计系统时不能忽视这个事实,尤其是当我们打算设计出能够很好地适应其业务领域要求的软件时。如果变更管理不当,即使是最复杂、最深思熟虑的设计最终也会成为维护和发展的噩梦。本章讨论软件项目环境的变化如何影响设计决策以及如何相应地改进设计。我们将研究四种最常见的变革向量:业务领域、组织结构、领域知识和增长。
In the modern, fast-paced world we inhabit, companies cannot afford to be lethargic. To keep up with the competition, they have to continually change, evolve, and even reinvent themselves over time. We cannot ignore this fact when designing systems, especially if we intend to design software that’s well adapted to the requirements of its business domain. When changes are not managed properly, even the most sophisticated and thoughtful design will eventually become a nightmare to maintain and evolve. This chapter discusses how changes in a software project’s environment can affect design decisions and how to evolve the design accordingly. We will examine the four most common vectors of change: business domain, organizational structure, domain knowledge, and growth.
在第 2 章中,您学习了三个业务子域的类型以及它们之间的区别:
In Chapter 2, you’ve learned the three types of business subdomains and how they are different from one another:
在前面的章节中,您看到了起作用的子域类型会影响战略和战术设计决策:
In the previous chapters, you saw that the type of subdomain at play affects strategic and tactical design decisions:
如何设计限界上下文的边界
How to design the bounded contexts’ boundaries
如何协调上下文之间的集成
How to orchestrate integration between the contexts
使用哪些设计模式来适应业务逻辑的复杂性
Which design patterns to use to accommodate the complexity of the business logic
要设计受业务领域需求驱动的软件,识别业务子域及其类型至关重要。然而,这还不是全部。警惕子域的演变同样重要。随着组织的成长和发展,其某些子域从一种类型转变为另一种类型的情况并不少见。让我们看一下此类更改的一些示例。
To design software that is driven by the business domain’s needs, it’s crucial to identify the business subdomains and their types. However, that’s not the whole story. It’s equally important to be alert to the evolution of the subdomains. As an organization grows and evolves, it’s not unusual for some of its subdomains to morph from one type to another. Let’s look at some examples of such changes.
想象一下,一家名为 BuyIT 的在线零售公司一直在实施自己的订单交付解决方案。它开发了一种创新算法来优化其快递员的送货路线,因此能够收取比竞争对手更低的送货费用。
Imagine that an online retail company called BuyIT has been implementing its own order delivery solution. It developed an innovative algorithm to optimize its couriers’ delivery routes and thus is able to charge lower delivery fees than its competitors.
有一天,另一家公司 DeliverIT 颠覆了快递行业。它声称已经解决了“旅行商”问题,并提供路径优化服务。DeliverIT 的优化不仅更先进,而且其价格仅为执行相同任务的 BuyIT 成本的一小部分。
One day, another company—DeliverIT—disrupts the delivery industry. It claims it has solved the “traveling salesman” problem and provides path optimization as a service. Not only is DeliverIT’s optimization more advanced, it is offered at a fraction of the price that it costs BuyIT to perform the same task.
从 BuyIT 的角度来看,一旦 DeliverIT 的解决方案作为现成产品可用,其核心子域就会变成通用子域。因此,最佳解决方案可供 BuyIT 的所有竞争对手使用。如果没有大量的研发投资,BuyIT 将无法在路径优化子领域获得竞争优势。以前被认为是 BuyIT 竞争优势的东西现在变成了所有竞争对手都可以获得的商品。
From BuyIT’s perspective, once DeliverIT’s solution became available as an off-the-shelf product, its core subdomain turned into a generic subdomain. As a result, the optimal solution became available to all of BuyIT’s competitors. Without massive investments in research and development, BuyIT can no longer gain a competitive advantage in the path optimization subdomain. What was previously considered a competitive advantage for BuyIT has become a commodity available to all of its competitors.
自成立以来,BuyIT 一直使用现成的解决方案来管理其库存。然而,其商业智能报告不断显示其对客户需求的预测不足。因此,BuyIT 无法补充其最受欢迎产品的库存,并在不受欢迎的产品上浪费仓库空间。在评估了几个备选库存管理解决方案之后,BuyIT 的管理团队做出了投资设计和构建内部系统的战略决策。该内部解决方案将考虑 BuyIT 销售的产品的复杂性,并更好地预测客户的需求。
Since its inception, BuyIT has been using an off-the-shelf solution to manage its inventory. However, its business intelligence reports are continuously showing inadequate predictions of its customers’ demands. Consequently, BuyIT fails to replenish its stock of the most popular products and is wasting warehouse real estate on the unpopular products. After evaluating a few alternative inventory management solutions, BuyIT’s management team makes the strategic decision to invest in designing and building an in-house system. This in-house solution will consider the intricacies of the products BuyIT sells and make better predictions of customers’ demands.
BuyIT 决定用自己的实施替换现成的解决方案,将库存管理从通用子域转变为核心子域:该功能的成功实施将为 BuyIT 提供超过其竞争对手的额外竞争优势——竞争对手将继续“停滞不前”使用通用解决方案,将无法使用 BuyIT 发明和开发的高级需求预测算法。
BuyIT’s decision to replace the off-the-shelf solution with its own implementation has turned inventory management from a generic subdomain into a core subdomain: successful implementation of the functionality will provide BuyIT additional competitive advantage over its competitors—the competitors will remain “stuck” with the generic solution and will not be able to use the advanced demand prediction algorithms invented and developed by BuyIT.
公司将通用子域转变为核心子域的真实教科书示例是亚马逊。与所有服务提供商一样,亚马逊需要一个基础设施来运行其服务。该公司能够“重塑”其管理物理基础设施的方式,后来甚至将其转变为盈利业务:Amazon Web Services。
A real-life textbook example of a company turning a generic subdomain into a core subdomain is Amazon. Like all service providers, Amazon needed an infrastructure on which to run its services. The company was able to “reinvent” the way it managed its physical infrastructure and later even turned it into a profitable business: Amazon Web Services.
BuyIT 的营销部门实施了一个管理系统与它合作的供应商及其合同。该系统没有什么特别或复杂的地方——它只是一些用于输入数据的 CRUD 用户界面。换句话说,它是一个典型的支持子域。
BuyIT’s marketing department implements a system for managing the vendors it works with and their contracts. There is nothing special or complex about the system—it’s just some CRUD user interfaces for entering data. In other words, it is a typical supporting subdomain.
然而,在 BuyIT 开始实施内部解决方案几年后,出现了一个开源合同管理解决方案。该开源项目实现了与现有解决方案相同的功能,并具有更高级的功能,例如 OCR 和全文搜索。这些附加功能长期以来一直在 BuyIT 的积压工作中,但由于它们对业务的影响很小,因此从未被优先考虑。因此,该公司决定放弃内部解决方案,转而采用集成开源解决方案。这样做时,文档管理子域从支持子域转变为通用子域。
However, a few years after BuyIT began implementing the in-house solution, an open source contracts management solution came out. The open source project implements the same functionality as the existing solution and has more advanced features, like OCR and full-text search. These additional features had been on BuyIT’s backlog for a long time but were never prioritized because of their low business impact. Hence, the company decides to ditch the in-house solution in favor of integrating the open source solution. In doing so, the document management subdomain turns from a supporting into a generic subdomain.
支持子域也可以变成核心子域——例如,如果一家公司找到一种方法来优化支持逻辑,从而降低成本或产生额外利润。
A supporting subdomain can also turn into a core subdomain—for example, if a company finds a way to optimize the supporting logic in such a way that it either reduces costs or generates additional profits.
这种转变的典型症状是支持子域的业务逻辑越来越复杂。支持子域,顾名思义,很简单,主要类似于CRUD接口或者ETL流程。但是,如果业务逻辑随着时间的推移变得更加复杂,那么增加复杂性应该是有原因的。如果不影响公司的利润,为什么会变得更复杂?这是偶然的业务复杂性。另一方面,如果它提高了公司的盈利能力,则表明支持子域成为核心子域。
The typical symptom of such a transformation is the increasing complexity of the supporting subdomain’s business logic. Supporting subdomains, by definition, are simple, mainly resembling CRUD interfaces or ETL processes. However, if the business logic becomes more complicated over time, there should be a reason for the additional complexity. If it doesn’t affect the company’s profits, why would it become more complicated? That’s accidental business complexity. On the other hand, if it enhances the company’s profitability, it’s a sign of a supporting subdomain becoming a core subdomain.
随着时间的推移,核心子域可以成为支持子域。当子域的复杂性不合理时,就会发生这种情况。换句话说,它不赚钱。在这种情况下,组织可能会决定削减无关的复杂性,留下支持其他子域实现所需的最少逻辑。
A core subdomain can, over time, become a supporting subdomain. This can happen when the subdomain’s complexity isn’t justified. In other words, it’s not profitable. In such cases, the organization may decide to cut the extraneous complexity, leaving the minimum logic needed to support implementation of other subdomains.
最后,出于与核心子域相同的原因,通用子域可以变成辅助。回到 BuyIT 的文档管理系统的例子,假设公司认为集成开源解决方案的复杂性并不能证明收益是合理的,并且已经求助于内部系统。结果,通用子域变成了支持子域。
Finally, for the same reason as a core subdomain, a generic subdomain can turn into a supporting one. Going back to the example of BuyIT’s document management system, assume the company has decided that the complexity of integrating the open source solution doesn’t justify the benefits and has resorted back to the in-house system. As a result, the generic subdomain has turned into a supporting subdomain.
图 11-1展示了我们刚才讨论的子域的变化。
The changes in subdomains we just discussed are demonstrated in Figure 11-1.
子域类型的更改直接影响其有界上下文,并且,因此,相应的战略设计决策。正如您在第 4 章中了解到的,不同的限界上下文集成模式适用于不同的子域类型。核心子域必须通过使用反腐败层来保护其模型,并且必须通过使用已发布的语言 (OHS) 来保护消费者免受实施模型中频繁更改的影响。
A change in a subdomain’s type directly affects its bounded context and, consequently, corresponding strategic design decisions. As you learned in Chapter 4, different bounded context integration patterns accommodate the different subdomain types. The core subdomains have to protect their models by using anticorruption layers and have to protect consumers from frequent changes in the implementation models by using published languages (OHS).
受此类更改影响的另一种集成模式是分离方式模式。正如您之前看到的,团队可以将此模式用于支持和通用子域。如果子域转变为核心子域,则不再允许多个团队复制其功能。因此,团队别无选择,只能集成他们的实现。在这种情况下,客户-供应商关系最有意义,因为核心子域将仅由一个团队实施。
Another integration pattern that is affected by such changes is the separate ways pattern. As you saw earlier, teams can use this pattern for supporting and generic subdomains. If the subdomain morphs into a core subdomain, duplicating its functionality by multiple teams is no longer acceptable. Hence, the teams have no choice but to integrate their implementations. The customer–supplier relationship will make the most sense in this case, since the core subdomain will only be implemented by one team.
从实施策略的角度来看,核心子域和支持子域的实施方式不同。支持子域可以外包或用作新员工的“培训轮”。核心子域必须在内部实施,尽可能靠近领域知识的来源。因此,当支持子域变成核心子域时,其实现应移至内部。同样的逻辑反过来也适用。如果核心子域变成支持子域,则可以将实现外包,让内部研发团队专注于核心子域。
From an implementation strategy standpoint, core and supporting subdomains differ in how they can be implemented. Supporting subdomains can be outsourced or used as “training wheels” for new hires. Core subdomains must be implemented in-house, as close as possible to the sources of domain knowledge. Therefore, when a supporting subdomain turns into a core subdomain, its implementation should be moved in-house. The same logic works the other way around. If a core subdomain turns into a supporting subdomain, it’s possible to outsource the implementation to let the in-house R&D teams concentrate on the core subdomains.
子域类型变化的主要指标是无法现有的技术设计,以支持当前的业务需求。
The main indicator of a change in a subdomain’s type is the inability of the existing technical design to support current business needs.
让我们回到支持子域成为核心子域的示例。支持子域是用相对简单的设计模式实现的,用于对业务逻辑进行建模:即事务脚本或活动记录模式。正如您在第 5 章中看到的,这些模式不太适合涉及复杂规则和不变量的业务逻辑。
Let’s go back to the example of a supporting subdomain becoming a core subdomain. Supporting subdomains are implemented with relatively simple design patterns for modeling the business logic: namely, the transaction script or active record pattern. As you saw in Chapter 5, these patterns are not a good fit for business logic involving complex rules and invariants.
如果随着时间的推移向业务逻辑添加复杂的规则和不变量,代码库也会变得越来越复杂。添加新功能会很痛苦,因为设计不支持新级别的复杂性。这种“痛”是一个重要的信号。将其用作重新评估业务领域和设计选择的调用。
If complicated rules and invariants are added to the business logic over time, the codebase will become increasingly complex as well. It will be painful to add the new functionality, as the design won’t support the new level of complexity. This “pain” is an important signal. Use it as a call to reassess the business domain and design choices.
改变实施策略的必要性并不可怕。这是正常的。我们无法预见企业将如何发展。我们也无法为所有类型的子域应用最精细的设计模式;那将是浪费和无效的。我们必须选择最合适的设计并在需要时对其进行改进。
The need for change in the implementation strategy is nothing to fear. It’s normal. We cannot foresee how a business will evolve down the road. We also cannot apply the most elaborate design patterns for all types of subdomains; that would be wasteful and ineffective. We have to choose the most appropriate design and evolve it when needed.
如果有意识地决定如何对业务逻辑建模,并且您了解所有可能的设计选择以及它们之间的差异,那么从一种设计模式迁移到另一种设计模式就不会那么麻烦。以下小节重点介绍了几个示例。
If the decision for how to model the business logic is made consciously, and you are aware of all the possible design choices and the differences between them, migrating from one design pattern to another is not that troublesome. The following subsections highlight a few examples.
它们的核心是事务脚本和活动记录模式它们基于相同的原则:业务逻辑是作为程序脚本实现的。它们之间的区别在于数据结构的建模方式:活动记录模式引入数据结构来封装将它们映射到存储机制的复杂性。
At their core, both the transaction script and active record patterns are based on the same principle: the business logic is implemented as a procedural script. The difference between them is how the data structures are modeled: the active record pattern introduces the data structures to encapsulate the complexity of mapping them to the storage mechanism.
因此,当在事务脚本中处理数据变得具有挑战性时,将其重构为活动记录模式。寻找复杂的数据结构并将它们封装在活动记录对象中。不是直接访问数据库,而是使用活动记录来抽象它的模型和结构。
As a result, when working with data becomes challenging in a transaction script, refactor it into the active record pattern. Look for complicated data structures and encapsulate them in active record objects. Instead of accessing the database directly, use active records to abstract its model and structure.
如果操作活动记录的业务逻辑变为复杂,并且您注意到越来越多的不一致和重复情况,将实现重构为域模型模式。
If the business logic that manipulates active records becomes complex and you notice more and more cases of inconsistencies and duplications, refactor the implementation to the domain model pattern.
从识别价值对象开始。哪些数据结构可以建模为不可变对象?寻找相关的业务逻辑,并将其也作为值对象的一部分。
Start by identifying value objects. What data structures can be modeled as immutable objects? Look for the related business logic, and make it a part of the value objects as well.
接下来,分析数据结构并寻找事务边界。为确保所有状态修改逻辑都是明确的,请将所有活动记录的设置程序设为私有,以便只能从活动记录本身内部修改它们。显然,期望编译失败;然而,编译错误将清楚地表明状态修改逻辑所在的位置。将其重构为活动记录的边界。例如:
Next, analyze the data structures and look for transactional boundaries. To ensure that all state-modifying logic is explicit, make all of the active records’ setters private so that they can only be modified from inside the active record itself. Obviously, expect the compilation to fail; however, the compilation errors will make it clear where the state-modifying logic resides. Refactor it into the active record’s boundaries. For example:
publicclassPlayer{publicGuidId{get;set;}publicintPoints{get;set;}}publicclassApplyBonus{...publicvoidExecute(GuidplayerId,bytepercentage){varplayer=_repository.Load(playerId);player.Points*=1+percentage/100.0;_repository.Save(player);}}
publicclassPlayer{publicGuidId{get;set;}publicintPoints{get;set;}}publicclassApplyBonus{...publicvoidExecute(GuidplayerId,bytepercentage){varplayer=_repository.Load(playerId);player.Points*=1+percentage/100.0;_repository.Save(player);}}
在下面的代码中,您可以看到转换的第一步。代码还不能编译,但错误会明确说明外部组件控制对象状态的位置:
In the following code, you can see the first steps toward the transformation. The code won’t compile yet, but the errors will make it explicit where external components are controlling the object’s state:
publicclassPlayer{publicGuidId{get;privateset;}publicintPoints{get;privateset;}}publicclassApplyBonus{...publicvoidExecute(GuidplayerId,bytepercentage){varplayer=_repository.Load(playerId);player.Points*=1+percentage/100.0;_repository.Save(player);}}
publicclassPlayer{publicGuidId{get;privateset;}publicintPoints{get;privateset;}}publicclassApplyBonus{...publicvoidExecute(GuidplayerId,bytepercentage){varplayer=_repository.Load(playerId);player.Points*=1+percentage/100.0;_repository.Save(player);}}
在下一次迭代中,我们可以将该逻辑移动到活动记录的边界内:
In the next iteration, we can move that logic inside the active record’s boundary:
publicclassPlayer{publicGuidId{get;privateset;}publicintPoints{get;privateset;}publicvoidApplyBonus(intpercentage){this.Points*=1+percentage/100.0;}}
publicclassPlayer{publicGuidId{get;privateset;}publicintPoints{get;privateset;}publicvoidApplyBonus(intpercentage){this.Points*=1+percentage/100.0;}}
当所有状态修改业务逻辑都移动到相应对象的边界内时,检查需要哪些层次结构来确保对业务规则和不变量的检查具有强一致性。这些是聚合的良好候选者。牢记我们在第 6 章中讨论的聚合设计原则,寻找最小的事务边界,即您需要保持强一致性的最小数据量。沿着这些边界分解层次结构。确保外部聚合仅由其 ID 引用。
When all the state-modifying business logic is moved inside the boundaries of the corresponding objects, examine what hierarchies are needed to ensure strongly consistent checking of business rules and invariants. Those are good candidates for aggregates. Keeping in mind the aggregate design principles we discussed in Chapter 6, look for the smallest transaction boundaries, that is, the smallest amount of data that you need to keep strongly consistent. Decompose the hierarchies along those boundaries. Make sure the external aggregates are only referenced by their IDs.
最后,对于每个聚合,确定其根或其公共接口的入口点。将聚合中所有其他内部对象的方法设为私有,并且只能从聚合中调用。
Finally, for each aggregate, identify its root, or the entry point for its public interface. Make the methods of all the other internal objects in the aggregate private and only callable from within the aggregate.
一旦你有了一个具有正确设计的聚合边界的域模型,您可以将其转换为事件源模型。不是直接修改聚合的数据,而是对表示聚合生命周期所需的领域事件建模。
Once you have a domain model with properly designed aggregate boundaries, you can transition it to the event-sourced model. Instead of modifying the aggregate’s data directly, model the domain events needed to represent the aggregate’s lifecycle.
将领域模型重构为事件源领域模型最具挑战性的方面是现有聚合的历史:将“永恒”状态迁移到基于事件的模型中。由于表示所有过去状态更改的细粒度数据不存在,因此您必须尽最大努力生成过去的事件或对迁移事件进行建模。
The most challenging aspect of refactoring a domain model into an event-sourced domain model is the history of the existing aggregates: migrating the “timeless” state into the event-based model. Since the fine-grained data representing all the past state changes is not there, you have to either generate past events on a best-effort basis or model migration events.
这种方法需要生成一个近似的事件流对于每个聚合,以便可以将事件流投影到与原始实现中相同的状态表示中。考虑您在第 7 章中看到的示例,如表 11-1所示。
This approach entails generating an approximate stream of events for each aggregate so that the stream of events can be projected into the same state representation as in the original implementation. Consider the example you saw in Chapter 7, as represented in Table 11-1.
| 带入 | 名 | 姓 | 电话号码 | 地位 | 最后联系时间 | 已下订单 | 开启 | 跟进 |
|---|---|---|---|---|---|---|---|---|
| 12 | 绍纳 | 麦西亚 | 555-4753 | 转换 | 2020- 05-27 T 12:02:12.51Z | 2020- 05-27 T 12:02:12.51Z | 2020- 05-27 T 12:02:12.51Z | 无效的 |
我们可以从业务逻辑的角度假设聚合的实例已经初始化;然后联系了这个人,下了订单,最后,由于状态是“已转换”,订单的付款已经确认。以下一组事件可以代表所有这些假设:
We can assume from the business logic perspective that the instance of the aggregate has been initialized; then the person has been contacted, an order has been placed, and finally, since the status was “converted,” the payment for the order has been confirmed. The following set of events can represent all of these assumptions:
{"lead-id":12,"event-id":0,"event-type":"lead-initialized","first-name":"Shauna","last-name":"Mercia","phone-number":"555-4753"},{"lead-id":12,"event-id":1,"event-type":"contacted","timestamp":"2020-05-27T12:02:12.51Z"},{"lead-id":12,"event-id":2,"event-type":"order-submitted","payment-deadline":"2020-05-30T12:02:12.51Z","timestamp":"2020-05-27T12:02:12.51Z"},{"lead-id":12,"event-id":3,"event-type":"payment-confirmed","status":"converted","timestamp":"2020-05-27T12:38:44.12Z"}
{"lead-id":12,"event-id":0,"event-type":"lead-initialized","first-name":"Shauna","last-name":"Mercia","phone-number":"555-4753"},{"lead-id":12,"event-id":1,"event-type":"contacted","timestamp":"2020-05-27T12:02:12.51Z"},{"lead-id":12,"event-id":2,"event-type":"order-submitted","payment-deadline":"2020-05-30T12:02:12.51Z","timestamp":"2020-05-27T12:02:12.51Z"},{"lead-id":12,"event-id":3,"event-type":"payment-confirmed","status":"converted","timestamp":"2020-05-27T12:38:44.12Z"}
当一个一个地应用时,这些事件可以被投射到与原始系统中一样的精确状态表示中。通过投影状态并将其与原始数据进行比较,可以轻松测试“恢复”的事件。
When applied one by one, these events can be projected into the exact state representation as in the original system. The “recovered” events can be easily tested by projecting the state and comparing it to the original data.
但是,重要的是要记住这种方法的缺点。使用事件溯源的目标是获得可靠、高度一致的聚合域事件历史记录。使用这种方法时,不可能恢复状态转换的完整历史记录。在前面的示例中,我们不知道销售代理与此人联系了多少次,因此也不知道我们错过了多少“联系”事件。
However, it’s important to keep in mind the disadvantage of this approach. The goal of using event sourcing is to have a reliable, strongly consistent history of the aggregates’ domain events. When this approach is used, it’s impossible to recover the complete history of state transitions. In the preceding example, we don’t know how many times the sales agent has contacted the person, and therefore, how many “contacted” events we have missed.
另一种方法是承认缺乏知识关于过去的事件并将其明确地建模为事件。与其恢复可能导致当前状态的事件,不如定义一个迁移事件并使用它来初始化现有聚合实例的事件流:
The alternative approach is to acknowledge the lack of knowledge about past events and explicitly model it as an event. Instead of recovering the events that may have led to the current state, define a migration event and use it to initialize the event streams of existing aggregate instances:
{"lead-id":12,"event-id":0,"event-type":"migrated-from-legacy","first-name":"Shauna","last-name":"Mercia","phone-number":"555-4753","status":"converted","last-contacted-on":"2020-05-27T12:02:12.51Z","order-placed-on":"2020-05-27T12:02:12.51Z","converted-on":"2020-05-27T12:38:44.12Z","followup-on":null}
{"lead-id":12,"event-id":0,"event-type":"migrated-from-legacy","first-name":"Shauna","last-name":"Mercia","phone-number":"555-4753","status":"converted","last-contacted-on":"2020-05-27T12:02:12.51Z","order-placed-on":"2020-05-27T12:02:12.51Z","converted-on":"2020-05-27T12:38:44.12Z","followup-on":null}
这种方法的优点是它使过去数据的缺乏变得明确。在任何阶段,都不能有人错误地认为事件流捕获了聚合实例生命周期内发生的所有领域事件。缺点是遗留系统的痕迹将永远保留在事件存储中。例如,如果您正在使用 CQRS 模式(并且您很可能会使用事件源域模型),则预测将始终必须考虑迁移事件。
The advantage of this approach is that it makes the lack of past data explicit. At no stage can someone mistakenly assume that the event stream captures all of the domain events that happened during the aggregate instance’s lifecycle. The disadvantage is that the traces of the legacy system will remain in the event store forever. For example, if you are using the CQRS pattern (and with the event-sourced domain model you most likely will), the projections will always have to take into account the migration events.
另一种会影响系统设计的变更是变更在组织本身。第 4 章研究了集成限界上下文的不同模式:伙伴关系、共享内核、遵从者、反腐败层、开放主机服务和分离方式。组织结构的变化会影响团队的沟通和协作水平,进而影响限界上下文的集成方式。
Another type of change that can affect a system’s design is a change in the organization itself. Chapter 4 looked at different patterns of integrating bounded contexts: partnership, shared kernel, conformist, anticorruption layer, open-host service, and separate ways. Changes in the organization’s structure can affect teams’ communication and collaboration levels and, as a result, the ways the bounded contexts should be integrated.
这种变化的一个简单例子是不断发展的开发中心,如图11-2所示。由于限界上下文只能由一个团队实现,添加新的开发团队可能会导致现有的更宽的限界上下文边界拆分成更小的边界,以便每个团队都可以在自己的限界上下文上工作。
A trivial example of such change is growing development centers, as shown in Figure 11-2. Since a bounded context can be implemented by only one team, adding new development teams can cause the existing wider bounded context boundaries to split into smaller ones so that each team can work on its own bounded context.
此外,组织的开发中心通常位于不同的地理位置。当现有限界上下文的工作转移到另一个位置时,可能会对团队的协作产生负面影响。因此,限界上下文的集成模式必须相应地发展,如以下场景所述。
Moreover, the organization’s development centers are often located in different geographical locations. When the work on the existing bounded contexts is shifted to another location, it may negatively impact the teams’ collaboration. As a result, the bounded contexts’ integration patterns have to evolve accordingly, as described in the following scenarios.
伙伴关系模式假设有很强的沟通以及团队之间的协作。随着时间的流逝,情况可能不再如此;例如,当对其中一个限界上下文的工作被转移到一个遥远的开发中心时。这种变化会对团队的沟通产生负面影响,从合作模式转向客户-供应商关系可能是有意义的。
The partnership pattern assumes there is strong communication and collaboration among teams. As time goes by, that might cease to be the case; for example, when work on one of the bounded contexts is moved to a distant development center. Such a change will negatively affect the teams’ communication, and it may make sense to move away from the partnership pattern toward a customer–supplier relationship.
不幸的是,团队遇到严重的沟通问题并不少见。这些问题可能是由地理距离或组织政治引起的。随着时间的推移,这样的团队可能会遇到越来越多的集成问题。在某些时候,复制功能而不是不断追逐对方的尾巴可能会变得更具成本效益。
Unfortunately, it’s not uncommon for teams to have severe communication problems. The issues might be caused by geographical distance or organizational politics. Such teams may experience more and more integration issues over time. At some point, it may become more cost-effective to duplicate the functionality instead of continuously chasing one another’s tails.
你会记得,领域驱动设计的核心原则是领域知识对于设计一个成功的软件系统是必不可少的。获取领域知识是软件工程中最具挑战性的方面之一,尤其是对于核心子领域。核心子域的逻辑不仅复杂,而且经常变化。此外,建模是一个持续的过程。随着业务领域知识的增加,模型必须得到改进。
As you’ll recall, the core tenet of domain-driven design is that domain knowledge is essential for designing a successful software system. Acquiring domain knowledge is one of the most challenging aspects of software engineering, especially for the core subdomains. A core subdomain’s logic is not only complicated, but also expected to change often. Moreover, modeling is an ongoing process. Models have to improve as more knowledge of the business domain is acquired.
很多时候,业务领域的复杂性是隐含的。最初,一切似乎都简单明了。最初的简单性往往具有欺骗性,并且很快就会变成复杂性。随着添加更多功能,会发现越来越多的边缘情况、不变量和规则。此类见解通常具有破坏性,需要从头开始重建模型,包括有界上下文的边界、聚合和其他实现细节。
Many times, the business domain’s complexity is implicit. Initially, everything seems simple and straightforward. The initial simplicity is often deceptive and it quickly morphs into complexity. As more functionality is added, more and more edge cases, invariants, and rules are discovered. Such insights are often disruptive, requiring rebuilding the model from the ground up, including the boundaries of the bounded contexts, aggregates, and other implementation details.
从战略设计的角度来看,根据领域知识的水平来设计限界上下文的边界是一种有用的启发式方法。将系统分解为限界上下文的成本可能会随着时间的推移而变得不正确。因此,当领域逻辑不明确且经常变化时,设计具有更宽边界的限界上下文是有意义的。然后,随着时间的推移发现领域知识并且业务逻辑的变化稳定下来,这些广泛的有界上下文可以分解为具有更窄边界或微服务的上下文。我们将在第 14 章中更详细地讨论限界上下文和微服务之间的相互作用。
From a strategic design standpoint, it’s a useful heuristic to design the bounded contexts’ boundaries according to the level of domain knowledge. The cost of decomposing a system into bounded contexts that, over time, turn out to be incorrect can be high. Therefore, when the domain logic is unclear and changes often, it makes sense to design the bounded contexts with broader boundaries. Then, as domain knowledge is discovered over time and changes to the business logic stabilize, those broad bounded contexts can be decomposed into contexts with narrower boundaries, or microservices. We will discuss the interplay between bounded contexts and microservices in more detail in Chapter 14.
当发现新的领域知识时,应该利用它来改进设计并使其更具弹性。不幸的是,领域知识的变化并不总是积极的:领域知识可能会丢失。随着时间的流逝,文档通常会变得陈旧,从事原始设计的人员离开公司,并且以临时方式添加新功能,直到代码库一度成为遗留系统的可疑状态。主动防止领域知识的这种退化至关重要。恢复领域知识的有效工具是 EventStorming 研讨会,这是下一章的主题。
When new domain knowledge is discovered, it should be leveraged to evolve the design and make it more resilient. Unfortunately, changes in domain knowledge are not always positive: domain knowledge can be lost. As time goes by, documentation often becomes stale, people who were working on the original design leave the company, and new functionality is added in an ad hoc manner until, at one point, the codebase gains the dubious status of a legacy system. It’s vital to prevent such degradation of domain knowledge proactively. An effective tool for recovering domain knowledge is the EventStorming workshop, which is the topic of the next chapter.
增长是健康系统的标志。当新功能不断添加,这是系统成功的标志:它为用户带来了价值,并不断扩展以进一步满足用户的需求并跟上竞争产品的步伐。但增长也有阴暗面。随着软件项目的增长,它的代码库可能会变成一个大泥球:
Growth is a sign of a healthy system. When new functionality is continuously added, it’s a sign that the system is successful: it brings value to its users and is expanded to further address users’ needs and keep up with competing products. But growth has a dark side. As a software project grows, its codebase can grow into a big ball of mud:
一个大泥球是一个结构随意、蔓延、草率、胶带和捆扎线、意大利面条代码丛林。这些系统毫无疑问地显示出不受控制的增长迹象,以及反复的权宜之计修复。
布赖恩·富特和约瑟夫·尤德1
A big ball of mud is a haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle. These systems show unmistakable signs of unregulated growth, and repeated, expedient repair.
Brian Foote and Joseph Yoder1
导致大泥球的不受管制的增长源于扩展软件系统的功能而不重新评估其设计决策。增长打破了组件的边界,越来越多地扩展了它们的功能。检查增长对设计决策的影响至关重要,尤其是因为许多领域驱动的设计工具都是关于设置边界的:业务构建块(子域)、模型(有界上下文)、不变性(值对象)或一致性(聚合) .
The unregulated growth that leads to big balls of mud results from extending a software system’s functionality without re-evaluating its design decisions. Growth blows up the components’ boundaries, increasingly extending their functionality. It’s crucial to examine the effects of growth on design decisions, especially since many domain-driven design tools are all about setting boundaries: business building blocks (subdomains), model (bounded contexts), immutability (value objects), or consistency (aggregates).
处理增长驱动的复杂性的指导原则是识别和消除偶然的复杂性:由过时的设计决策引起的复杂性。应该使用领域驱动的设计工具和实践来管理业务领域的基本复杂性或固有复杂性。
The guiding principle for dealing with growth-driven complexity is to identify and eliminate accidental complexity: the complexity caused by outdated design decisions. The essential complexity, or inherent complexity of the business domain, should be managed using domain-driven design tools and practices.
我们在前面几章讨论DDD时,都是按照先分析业务领域及其战略组件,设计业务领域的相关模型,再设计并实现代码的流程。让我们按照相同的脚本来处理增长驱动的复杂性。
When we discuss DDD in earlier chapters, we follow the process of first analyzing the business domain and its strategic components, designing the relevant models of the business domain, and then designing and implementing the models in code. Let’s follow the same script for dealing with growth-driven complexity.
正如我们在第 1 章中讨论的那样,子域的边界识别可能具有挑战性,因此,我们必须努力寻找有用的界限,而不是努力寻找完美的界限。也就是说,子域应该允许我们识别不同业务价值的组件,并使用适当的工具来设计和实施解决方案。
As we discussed in Chapter 1, the subdomains’ boundaries can be challenging to identify, and as a result, instead of striving for boundaries that are perfect, we must strive for boundaries that are useful. That is, the subdomains should allow us to identify components of different business value and use the appropriate tools to design and implement the solution.
随着业务领域的增长,子域的边界会变得更加模糊,从而更难识别跨越多个更细粒度子域的子域。因此,重要的是重新访问已识别的子域并遵循连贯用例(处理同一数据集的用例集)的启发式以尝试确定在何处拆分子域(参见图 11-3 )。
As the business domain grows, the subdomains’ boundaries can become even more blurred, making it harder to identify cases of a subdomain spanning multiple, finer-grained subdomains. Hence, it’s important to revisit the identified subdomains and follow the heuristic of coherent use cases (sets of use cases working on the same set of data) to try to identify where to split a subdomain (see Figure 11-3).
如果您能够识别不同类型的更细粒度的子域,这是一个重要的洞察力,将使您能够管理业务域的基本复杂性。有关子域及其类型的信息越精确,您在为每个子域选择技术解决方案时就越有效。
If you are able to identify finer-grained subdomains of different types, this is an important insight that will allow you to manage the business domain’s essential complexity. The more precise the information about the subdomains and their types is, the more effective you will be at choosing technical solutions for each subdomain.
识别可以提取并显式显示的内部子域对于核心子域尤为重要。我们应该始终致力于尽可能多地从所有其他子域中提取核心子域,以便我们可以从业务战略的角度将我们的精力投入到最重要的地方。
Identifying inner subdomains that can be extracted and made explicit is especially important for core subdomains. We should always aim to distill core subdomains as much as possible from all others so that we can invest our effort where it matters most from a business strategy perspective.
在第 3 章中,您了解到限界上下文模式允许我们使用业务领域的不同模型。我们可以构建多个模型,而不是构建一个“万事通,无精通”的模型,每个模型都专注于解决一个特定的问题。
In Chapter 3, you learned that the bounded context pattern allows us to use different models of the business domain. Instead of building a “jack of all trades, master of none” model, we can build multiple models, each focused on solving a specific problem.
随着项目的发展和壮大,限界上下文失去焦点并积累与不同问题相关的逻辑的情况并不少见。那是偶然的复杂性。与子域一样,不时重新访问限界上下文的边界至关重要。始终寻找机会通过提取专注于解决特定问题的限界上下文来简化模型。
As a project evolves and grows, it’s not uncommon for the bounded contexts to lose their focus and accumulate logic related to different problems. That’s accidental complexity. As with subdomains, it’s crucial to revisit the bounded contexts’ boundaries from time to time. Always look for opportunities to simplify the models by extracting bounded contexts that are laser focused at solving specific problems.
增长还可以使现有的隐性设计问题显性化。例如,您可能会注意到随着时间的推移,许多有界上下文变得越来越“喋喋不休”,如果不调用另一个有界上下文就无法完成任何操作。这可能是一个无效模型的强烈信号,应该通过重新设计限界上下文的边界来增加它们的自主性来解决。
Growth can also make existing implicit design issues explicit. For example, you may notice that a number of bounded contexts become increasingly “chatty” over time, unable to complete any operation without calling another bounded context. That can be a strong signal of an ineffective model and should be addressed by redesigning the bounded contexts’ boundaries to increase their autonomy.
当我们在第 6 章讨论领域模型模式时,我们使用了以下指导原则来设计聚合的边界:
When we discussed the domain model pattern in Chapter 6, we used the following guiding principle for designing aggregates’ boundaries:
经验法则是使聚合尽可能小,并且仅包含业务领域要求处于高度一致状态的对象。
The rule of thumb is to keep the aggregates as small as possible and include only objects that are required to be in a strongly consistent state by the business domain.
随着系统业务需求的增长,在现有聚合之间分配新功能可能会很“方便”,而无需重新考虑保持聚合较小的原则。如果聚合增长到包含不需要与其所有业务逻辑高度一致的数据,那么,这是必须消除的偶然复杂性。
As the system’s business requirements grow, it can be “convenient” to distribute the new functionalities among the existing aggregates, without revisiting the principle of keeping aggregates small. If an aggregate grows to include data that is not needed to be strongly consistent by all of its business logic, again, that’s accidental complexity that has to be eliminated.
将业务功能提取到专用聚合中不仅可以简化原始聚合,还可以简化它所属的限界上下文。通常,这种重构会发现一个额外的隐藏模型,一旦明确,就应该将其提取到不同的有界上下文中。
Extracting business functionality into a dedicated aggregate not only simplifies the original aggregate, but potentially can simplify the bounded context it belongs to. Often, such refactoring uncovers an additional hidden model that, once made explicit, should be extracted into a different bounded context.
正如赫拉克利特的名言,生活中唯一不变的就是变化。企业也不例外。为了保持竞争力,公司不断努力发展和重塑自己。这些变化应该被视为设计过程的首要元素。
As Heraclitus famously said, the only constant in life is change. Businesses are no exception. To stay competitive, companies constantly strive to evolve and reinvent themselves. Those changes should be treated as first-class elements of the design process.
随着业务领域的发展,必须在系统设计中识别并处理对其子域的更改。确保您过去的设计决策与业务领域及其子领域的当前状态保持一致。在需要时,改进您的设计以更好地匹配当前的业务战略和需求。
As the business domain evolves, changes to its subdomains must be identified and acted on in the system’s design. Make sure your past design decisions are aligned with the current state of the business domain and its subdomains. When needed, evolve your design to better match the current business strategy and needs.
同样重要的是要认识到组织结构的变化会影响团队之间的沟通和合作以及他们的限界上下文的集成方式。了解业务领域是一个持续的过程。随着时间的推移发现更多的领域知识,必须利用它来发展战略和战术设计决策。
It’s also important to recognize that changes in the organizational structure can affect communication and cooperation among teams and the ways their bounded contexts can be integrated. Learning about the business domain is an ongoing process. As more domain knowledge is discovered over time, it has to be leveraged to evolve strategic and tactical design decisions.
最后,软件增长是一种理想的变化类型,但如果管理不当,可能会对系统设计和架构造成灾难性影响。所以:
Finally, software growth is a desired type of change, but when it is not managed correctly, it may have disastrous effects on the system design and architecture. Therefore:
当扩展子域的功能时,尝试确定更细粒度的子域边界,这将使您能够做出更好的设计决策。
When a subdomain’s functionality is expanded, try to identify more finer-grained subdomain boundaries that will enable you to make better design decisions.
不要让限界上下文成为“万事通”。确保限界上下文包含的模型专注于解决特定问题。
Don’t allow a bounded context to become a “jack of all trades.” Make sure the models encompassed by bounded contexts are focused to solve specific problems.
确保聚合的边界尽可能小。使用强一致性数据的启发式方法来检测将业务逻辑提取到新聚合中的可能性。
Make sure your aggregates’ boundaries are as small as possible. Use the heuristic of strongly consistent data to detect possibilities to extract business logic into new aggregates.
关于该主题,我最后的智慧之言是不断检查不同边界以寻找增长驱动的复杂性迹象。采取行动消除偶然的复杂性,并使用领域驱动的设计工具来管理业务领域的基本复杂性。
My final words of wisdom on the topic are to continuously check the different boundaries for signs of growth-driven complexity. Act to eliminate accidental complexities, and use domain-driven design tools to manage the business domain’s essential complexity.
组织成长通常会导致限界上下文集成发生什么样的变化?
与客户-供应商的合作伙伴关系(守规矩、反腐败层或开放主机服务)
反腐层开放主机服务
符合共享内核
共享内核的开放主机服务
What kind of changes in bounded context integration are often caused by organizational growth?
Partnership to customer–supplier (conformist, anticorruption layer, or open-host service)
Anticorruption layer to open-host service
Conformist to shared kernel
Open-host service to shared kernel
假设限界上下文的集成从顺从关系转变为分离方式。根据变化你能推断出什么信息?
开发团队努力合作。
重复功能是支持或通用子域。
重复功能是一个核心子域。
A和B。
A和C。
Assume that the bounded contexts’ integration shifts from a conformist relationship to separate ways. What information can you deduce based on the change?
The development teams struggled to cooperate.
The duplicate functionality is either a supporting or a generic subdomain.
The duplicate functionality is a core subdomain.
A and B.
A and C.
支持子域成为核心子域的症状是什么?
发展现有模型和实施新要求变得更加容易。
发展现有模型变得很痛苦。
子域以更高的频率更改。
B和C。
以上都不是。
What are the symptoms of a supporting subdomain becoming a core subdomain?
It becomes easier to evolve the existing model and implement the new requirements.
It becomes painful to evolve the existing model.
The subdomain changes at a higher frequency.
B and C.
None of the above.
发现新的商业机会会带来什么变化?
支持子域变成核心子域。
支持子域变成通用子域。
通用子域变成核心子域。
通用子域变成支持子域。
A和B。
A和C。
What change results from discovering a new business opportunity?
A supporting subdomain turns into a core one.
A supporting subdomain turns into a generic one.
A generic subdomain turns into a core one.
A generic subdomain turns into a supporting one.
A and B.
A and C.
业务战略的哪些变化可以将 WolfDesk(前言中描述的虚构公司)的一个通用子域转变为核心子域?
What change in the business strategy could turn one of WolfDesk’s (the fictitious company described in the Preface) generic subdomains into a core subdomain?
在本章中,我们将暂停讨论软件设计模式和技术。相反,我们将专注于称为EventStorming的低技术建模过程。这个过程汇集了我们在前面章节中介绍的领域驱动设计的核心方面。
In this chapter, we will take a break from discussing software design patterns and techniques. Instead, we will focus on a low-tech modeling process called EventStorming. This process brings together the core aspects of domain-driven design that we covered in the preceding chapters.
您将学习 EventStorming 流程、如何促进 EventStorming 研讨会,以及如何利用 EventStorming 有效地共享领域知识和构建通用语言。
You will learn the EventStorming process, how to facilitate an EventStorming workshop, and how to leverage EventStorming to effectively share domain knowledge and build a ubiquitous language.
EventStorming 是针对一群人的低技术活动集思广益并快速建模业务流程。从某种意义上说,EventStorming 是一种用于共享业务领域知识的战术工具。
EventStorming is a low-tech activity for a group of people to brainstorm and rapidly model a business process. In a sense, EventStorming is a tactical tool for sharing business domain knowledge.
EventStorming 会话有一个范围:业务流程该小组有兴趣探索。参与者正在探索作为一系列域事件的过程,在时间轴上以便签表示。逐步地,该模型通过附加概念(参与者、命令、外部系统等)得到增强,直到其所有元素都讲述了业务流程如何工作的故事。
An EventStorming session has a scope: the business process that the group is interested in exploring. The participants are exploring the process as a series of domain events, represented by sticky notes, over a timeline. Step by step, the model is enhanced with additional concepts—actors, commands, external systems, and others—until all of its elements tell the story of how the business process works.
请记住,研讨会的目标是学习在尽可能短的时间内尽可能多。我们邀请关键人物参加研讨会,我们不想浪费他们宝贵的时间。
EventStorming研讨会的创建者 Alberto Brandolini
Just keep in mind that the goal of the workshop is to learn as much as possible in the shortest time possible. We invite key people to the workshop, and we don’t want to waste their valuable time.
Alberto Brandolini, creator of the EventStorming workshop
理想情况下,多元化的人群应该参加研讨会。实际上,与相关业务领域相关的任何人都可以参与:工程师、领域专家、产品所有者、测试人员、UI/UX 设计师、支持人员等。随着更多不同背景的人参与进来,就会发现更多的知识。
Ideally, a diverse group of people should participate in the workshop. Indeed, anyone related to the business domain in question can participate: engineers, domain experts, product owners, testers, UI/UX designers, support personnel, and so on. As more people with different backgrounds are involved, more knowledge will be discovered.
但是,请注意不要使组太大。每个参与者都应该能够为该过程做出贡献,但这对于超过 10 名参与者的团体来说可能具有挑战性。
Take care not to make the group too big, however. Every participant should be able to contribute to the process, but this can be challenging for groups of more than 10 participants.
EventStorming 被认为是一个低技术车间,因为它已经完成使用笔和纸——实际上是很多纸。让我们看看您需要什么来促进 EventStorming 会话:
EventStorming is considered a low-tech workshop because it is done using a pen and paper—a lot of paper, actually. Let’s see what you need in order to facilitate an EventStorming session:
EventStorming 研讨会通常分 10 个步骤进行。在每个步骤中,模型都会用额外的信息和概念来丰富。
An EventStorming workshop is usually conducted in 10 steps. During each step, the model is enriched with additional information and concepts.
EventStorming 从领域事件的头脑风暴开始与正在探索的业务领域相关。领域事件是业务中发生的有趣事件。用过去时来表述领域事件很重要(见图12-2)——它们描述的是已经发生的事情。
EventStorming starts with a brainstorm of the domain events related to the business domain being explored. A domain event is something interesting that has happened in the business. It’s important to formulate domain events in the past tense (see Figure 12-2)—they are describing things that have already happened.
在此步骤中,所有参与者都拿了一堆橙色的便利贴,写下想到的任何领域事件,并将它们贴在建模表面上。
During this step, all participants are grabbing a bunch of orange sticky notes, writing down whatever domain events come to mind, and sticking them to the modeling surface.
在这个早期阶段,无需担心排序事件,甚至不必担心冗余。这一步是关于对业务领域中可能发生的事情进行头脑风暴。
At this early stage, there is no need to worry about ordering events, or even about redundancy. This step is all about brainstorming the possible things that can happen in the business domain.
该组应继续生成域事件,直到添加新事件的速度显着减慢。
The group should continue generating domain events until the rate of adding new ones slows significantly.
接下来,参与者检查生成的领域事件并组织它们按照它们在业务领域中出现的顺序排列。
Next, the participants go over the generated domain events and organize them in the order in which they occur in the business domain.
事件应从“快乐路径场景”开始:描述成功业务场景的流程。
The events should start with the “happy path scenario”: the flow that describes a successful business scenario.
一旦“快乐路径”完成,就可以添加备选场景——例如,遇到错误或做出不同业务决策的路径。流分支可以表示为来自先前事件的两个流或在建模表面上绘制的箭头,如图12-3所示。
Once the “happy path” is done, alternative scenarios can be added—for example, paths where errors are encountered or different business decisions are taken. The flow branching can be expressed as two flows coming from the preceding event or with arrows drawn on the modeling surface, as shown in Figure 12-3.
此步骤也是修复不正确事件、删除重复项以及必要时添加缺失事件的时间。
This step is also the time to fix incorrect events, remove duplicates, and of course, add missing events if necessary.
一旦你在时间轴上组织了事件,使用这个广泛的视图以确定过程中需要注意的点。这些可能是瓶颈、需要自动化的手动步骤、缺少文档或缺少领域知识。
Once you have the events organized in a timeline, use this broad view to identify points in the process that require attention. These can be bottlenecks, manual steps that require automation, missing documentation, or missing domain knowledge.
明确指出这些低效率很重要,这样在 EventStorming 会话进行时很容易返回到它们,或者在之后解决它们。痛点用旋转的(菱形)粉红色便利贴标记,如图12-4所示。
It’s important to make these inefficiencies explicit so that it will be easy to return to them as the EventStorming session progresses, or to address them afterward. The pain points are marked with rotated (diamond) pink sticky notes, as illustrated in Figure 12-4.
当然,这一步并不是追踪痛点的唯一机会。作为主持人,在整个过程中注意参与者的评论。当提出问题或疑虑时,将其记录为痛点。
Of course, this step is not the only opportunity to track pain points. As a facilitator, be aware of the participants’ comments throughout the process. When an issue or a concern is raised, document it as a pain point.
一旦你有了一个增加了痛点的事件时间表,寻找表明上下文或阶段发生变化的重要业务事件。这些被称为关键事件,并用竖线标记,将关键事件之前和之后的事件分开。
Once you have a timeline of events augmented with pain points, look for significant business events indicating a change in context or phase. These are called pivotal events and are marked with a vertical bar dividing the events before and after the pivotal event.
例如,“购物车已初始化”、“订单已初始化”、“订单已发货”、“订单已送达”和“订单已退回”表示下订单过程中的重大变化,如图 12-5所示。
For example, “shopping cart initialized,” “order initialized,” “order shipped,” “order delivered,” and “order returned” represent significant changes in the process of making an order, as shown in Figure 12-5.
关键事件是潜在有界上下文边界的指示器。
Pivotal events are an indicator of potential bounded context boundaries.
而领域事件描述的是已经发生的事情发生时,命令描述了是什么触发了事件或事件流。命令描述了系统的操作,并且与域事件相反,命令是命令式的。例如:
Whereas a domain event describes something that has already happened, a command describes what triggered the event or flow of events. Commands describe the system’s operations and, contrary to domain events, are formulated in the imperative. For example:
发布活动
Publish campaign
回滚事务
Roll back transaction
提交订单
Submit order
命令写在浅蓝色便签纸上,并在它们可以产生事件之前放置在建模空间中。如果特定命令由特定角色的参与者执行,参与者信息将添加到黄色小便签上的命令,如图12-6所示。参与者代表业务领域内的用户角色,例如客户、管理员或编辑。
Commands are written on light blue sticky notes and placed on the modeling space before the events they can produce. If a particular command is executed by an actor in a specific role, the actor information is added to the command on a small yellow sticky note, as illustrated in Figure 12-6. The actor represents a user persona within the business domain, such as customer, administrator, or editor.
当然,并非所有命令都有关联的参与者。因此,只在明显的地方添加演员信息。在下一步中,我们将使用可以触发命令的其他实体来扩充模型。
Naturally, not all commands will have an associated actor. Therefore, add the actor information only where it’s obvious. In the next step we will augment the model with additional entities that can trigger commands.
几乎总是,一些命令被添加到模型中,但是没有与他们相关联的特定演员。在此步骤中,您将寻找可能执行这些命令的自动化策略。
Almost always, some commands are added to the model but have no specific actor associated with them. During this step, you look for automation policies that might execute those commands.
自动化策略是事件触发命令执行的场景。换句话说,当特定领域事件发生时,命令会自动执行。
An automation policy is a scenario in which an event triggers the execution of a command. In other words, a command is automatically executed when a specific domain event occurs.
在建模表面上,策略表示为将事件连接到命令的紫色便签,如图12-7中的“Policy”便签所示。
On the modeling surface, policies are represented as purple sticky notes connecting events to commands, as shown by the “Policy” sticky note in Figure 12-7.
如果仅当满足某些决策标准时才应触发相关命令,您可以在策略便签上明确指定决策标准。例如,如果您需要escalate在“收到投诉”事件后触发该命令,但前提是收到来自 VIP 客户的投诉,您可以在策略贴上明确声明“仅适用于 VIP 客户”条件。
If the command in question should be triggered only if some decision criteria is met, you can specify the decision criteria explicitly on the policy sticky note. For example, if you need to trigger the escalate command after the “complaint received” event, but only if the complaint was received from a VIP customer, you can explicitly state the “only for VIP customers” condition on the policy sticky.
如果事件和命令相距较远,可以在建模面上画一个箭头将它们连接起来。
If the events and commands are far apart, you can draw an arrow on the modeling surface to connect them.
读取模型是参与者所在域内的数据视图用于做出执行命令的决定。这可以是系统的屏幕之一、报告、通知等。
A read model is the view of data within the domain that the actor uses to make a decision to execute a command. This can be one of the system’s screens, a report, a notification, and so on.
阅读模型由绿色便签表示(参见图 12-8中的“购物车”便签),其中包含支持参与者决策所需的信息来源的简短描述。由于命令是在参与者查看读取模型之后执行的,因此在建模表面上,读取模型位于命令之前。
The read models are represented by green sticky notes (see the “Shopping cart” note in Figure 12-8) with a short description of the source of information needed to support the actor’s decision. Since a command is executed after the actor has viewed the read model, on the modeling surface the read models are positioned before the commands.
此步骤是关于使用外部系统扩充模型。外部系统被定义为不属于正在探索的域的任何系统。它可以执行命令(输入)或可以通知事件(输出)。
This step is about augmenting the model with external systems. An external system is defined as any system that is not a part of the domain being explored. It can execute commands (input) or can be notified about events (output).
外部系统由粉红色便签表示。在图 12-9中,CRM(外部系统)触发执行“Ship Order”命令。当装运获得批准(事件)时,它会通过政策传达给 CRM(外部系统)。
The external systems are represented by pink sticky notes. In Figure 12-9, the CRM (external system) triggers execution of the “Ship Order” command. When the shipment is approved (event), it is communicated to the CRM (external system) through a policy.
到此步骤结束时,所有命令都应由参与者执行、由策略触发或由外部系统调用。
By the end of this step, all commands should either be executed by actors, triggered by policies, or called by external systems.
一旦所有的事件和命令都被表示出来,参与者可以开始考虑将相关概念组织起来。聚合接收命令并产生事件。
Once all the events and commands are represented, the participants can start thinking about organizing related concepts in aggregates. An aggregate receives commands and produces events.
聚合表示为黄色的大便签,左边是命令,右边是事件,如图12-10所示。
Aggregates are represented as large yellow sticky notes, with commands on the left and events on the right, as depicted in Figure 12-10.
EventStorming 会话的最后一步是查找聚合彼此相关,要么是因为它们代表密切相关的功能,要么是因为它们通过策略耦合。如图 12-11所示,聚合组形成有界上下文边界的自然候选者。
The last step of an EventStorming session is to look for aggregates that are related to each other, either because they represent closely related functionality or because they’re coupled through policies. The groups of aggregates form natural candidates for bounded contexts’ boundaries, as shown in Figure 12-11.
EventStorming 研讨会的创始人 Alberto Brandolini,将 EventStorming 过程定义为指南,而不是硬性规则。您可以自由试验该过程,以找到最适合您的“配方”。
Alberto Brandolini, the creator of the EventStorming workshop, defines the EventStorming process as guidance, not hard rules. You are free to experiment with the process to find the “recipe” that works best for you.
根据我的经验,在组织中引入 EventStorming 时,我更喜欢按照步骤 1(混乱探索)到 4(关键事件)探索业务领域的全局。由此产生的模型涵盖了公司的广泛业务领域,为无处不在的语言奠定了坚实的基础,并为限界上下文勾勒出了可能的边界。
In my experience, when introducing EventStorming in an organization I prefer to start by exploring the big picture of the business domain by following steps 1 (chaotic exploration) through 4 (pivotal events). The resultant model covers a wide range of the company’s business domain, builds a strong foundation for ubiquitous languages, and outlines possible boundaries for bounded contexts.
在了解全局并确定不同的业务流程后,我们继续为每个相关业务流程提供专门的 EventStorming 会话——这一次,按照所有步骤对完整流程进行建模。
After gaining the big picture and identifying the different business processes, we continue to facilitate a dedicated EventStorming session for each relevant business process—this time, following all the steps to model the complete process.
在完整的 EventStorming 会话结束时,您将拥有一个描述业务领域的事件、命令、聚合,甚至可能的限界上下文的模型。然而,所有这些都只是不错的奖励。EventStorming 会议的真正价值在于流程本身——不同利益相关者之间的知识共享、他们的业务心智模型的一致性、冲突模型的发现,以及最后但并非最不重要的是,通用语言的形成。
At the end of a full EventStorming session, you will have a model describing the business domain’s events, commands, aggregates, and even possible bounded contexts. However, all of these are just nice bonuses. The real value of an EventStorming session is the process itself—the sharing of knowledge among different stakeholders, alignment of their mental models of the business, discovery of conflicting models, and, last but not least, formulation of the ubiquitous language.
生成的模型可以作为实现事件源域模型的基础。是否走这条路的决定取决于您的业务领域。如果您决定实施事件源领域模型,您将拥有有界上下文边界、聚合,当然还有所需领域事件的蓝图。
The resultant model can be adopted as a basis for implementing an event-sourced domain model. The decision of whether to go that route or not depends on your business domain. If you decide to implement the event-sourced domain model, you have the bounded context boundaries, the aggregates, and of course, the blueprint of the required domain events.
The workshop can be facilitated for many reasons:
除了何时使用 EventStorming 之外,重要的是要提及何时不使用它。当您正在探索的业务流程简单或明显时,EventStorming 将不太成功,例如遵循一系列没有任何有趣的业务逻辑或复杂性的顺序步骤。
In addition to when to use EventStorming, it’s important to mention when not to use it. EventStorming will be less successful when the business process you’re exploring is simple or obvious, such as following a series of sequential steps without any interesting business logic or complexity.
促进与一群人的 EventStorming 会话时对于以前从未做过 EventStorming 的人,我更愿意从快速概述该过程开始。我解释了我们将要做什么、我们将要探索的业务流程以及我们将在研讨会中使用的建模元素。当我们浏览元素——域事件、命令、参与者等——我构建了一个图例,如图12-12所示,使用我们将使用的便签和标签来帮助参与者记住颜色代码。图例应该在研讨会期间对所有参与者可见。
When facilitating an EventStorming session with a group of people who have never done EventStorming before, I prefer to start with a quick overview of the process. I explain what we are about to do, the business process we are about to explore, and the modeling elements we will use in the workshop. As we go through the elements—domain events, commands, actors, and so on—I build a legend, depicted in Figure 12-12, using the sticky notes we will use and labels to help the participants remember the color code. The legend should be visible to all participants during the workshop.
随着研讨会的进行,跟踪小组的能量很重要。如果动力正在放缓,看看你是否可以通过提问来重新启动这个过程,或者是时候进入研讨会的下一阶段了。
As the workshop progresses, it’s important to track the energy of the group. If the dynamics are slowing down, see whether you can reignite the process by asking questions or whether it’s time to advance to the next stage of the workshop.
请记住,EventStorming 是一项集体活动,因此请确保按此处理。确保每个人都有机会参与建模和讨论。如果您注意到一些参与者回避小组,请尝试通过询问有关模型当前状态的问题让他们参与到过程中。
Remember that EventStorming is a group activity, so ensure that it is handled as such. Make sure everyone has a chance to participate in the modeling and the discussion. If you notice that some participants are shying away from the group, try to involve them in the process by asking questions about the current state of the model.
EventStorming 是一项激烈的活动,在某些时候,团队需要休息一下。在所有参与者都回到房间之前不要恢复会话。通过查看模型的当前状态来恢复该过程,使团队恢复到协作建模的氛围。
EventStorming is an intense activity, and at some point, the group will need a break. Don’t resume the session until all the participants are back in the room. Resume the process by going through the current state of the model to return the group to a collaborative modeling mood.
EventStorming 是作为一种低技术活动而发明的,人们可以在其中在同一个房间里一起互动和学习。研讨会的创建者 Alberto Brandolini 经常反对远程进行 EventStorming,因为当团队不在同一地点时,不可能实现相同级别的参与,因此无法实现协作和知识共享。
EventStorming was invented as a low-tech activity in which people interact and learn together in the same room. The creator of the workshop, Alberto Brandolini, has often objected to conducting EventStorming remotely because it’s impossible to achieve the same levels of participation, and hence, collaboration and knowledge sharing, when the group is not colocated.
然而,随着 2020 年 COVID-19 大流行的爆发,人们不可能按照原定的方式举行面对面的会议和进行 EventStorming。许多工具试图实现远程 EventStorming 会话的协作和便利。在撰写本文时,其中最著名的是miro.com。在进行在线 EventStorming 时要更有耐心,并考虑到由此导致的低效沟通。
However, with the onset of the COVID-19 pandemic in 2020, it became impossible to have in-person meetings and do EventStorming as it was meant to be done. A number of tools attempted to enable collaboration and facilitation of remote EventStorming sessions. At the time of this writing, the most notable of them is miro.com. Be more patient when doing online EventStorming and take into account the less effective communication that results.
此外,我的经验表明远程 EventStorming 会话在参与者数量较少的情况下更有效。虽然多达 10 人可以参加面对面的 EventStorming 会议,但我更愿意将在线会议限制为五名参与者。当您需要更多参与者贡献他们的知识时,您可以促进多个会话,然后比较和合并生成的模型。
In addition, my experience shows that remote EventStorming sessions are more effective with a smaller number of participants. While as many as 10 people can attend an in-person EventStorming session, I prefer to limit online sessions to five participants. When you need more participants to contribute their knowledge, you can facilitate multiple sessions, and afterward compare and merge the resultant models.
当情况允许时,返回到亲自的 EventStorming。
When the situation allows, return to in-person EventStorming.
EventStorming 是一个基于协作的业务流程建模研讨会。除了由此产生的模型,它的主要好处是知识共享。在会议结束时,所有参与者将同步他们的业务流程心智模型,并迈出使用通用语言的第一步。
EventStorming is a collaboration-based workshop for modeling business processes. Apart from the resultant models, its primary benefit is knowledge sharing. By the end of the session, all the participants will synchronize their mental models of the business process and take the first steps toward using a ubiquitous language.
EventStorming 就像骑自行车。通过实践来学习比在书中阅读要容易得多。尽管如此,研讨会很有趣,也很容易促进。您无需成为 EventStorming 黑带即可开始使用。只需促进会话,按照步骤操作,并在过程中学习。
EventStorming is like riding a bicycle. It’s much easier to learn by doing it than to read about it in a book. Nevertheless, the workshop is fun and easy to facilitate. You don’t need to be an EventStorming black belt to get started. Just facilitate the session, follow the steps, and learn during the process.
谁应该被邀请参加 EventStorming 会议?
软件工程师
领域专家
质检工程师
所有利益相关者都了解您要探索的业务领域
Who should be invited to an EventStorming session?
Software engineers
Domain experts
QA engineers
All stakeholders having knowledge of the business domain that you want to explore
什么时候是促进 EventStorming 会议的好机会?
建立一种无处不在的语言。
探索新的业务领域。
恢复遗失的棕地项目知识。
介绍新的团队成员。
发现优化业务流程的方法。
以上所有答案都是正确的。
When is it a good opportunity to facilitate an EventStorming session?
To build a ubiquitous language.
To explore a new business domain.
To recover lost knowledge of a brownfield project.
To introduce new team members.
To discover ways to optimize the business process.
All of the above answers are correct.
您可以从 EventStorming 会议中获得什么结果?
更好地共享对业务领域的理解
无处不在的语言的坚实基础
在业务领域的理解中发现的白点
可用于实现领域模型的基于事件的模型
以上所有,但取决于会议的目的
What outcomes can you expect from an EventStorming session?
A better shared understanding of the business domain
A strong foundation for a ubiquitous language
Uncovered white spots in the understanding of the business domain
An event-based model that can be used to implement a domain model
All of the above, but depending on the session’s purpose
我们介绍了用于分析业务领域的领域驱动设计工具,分享知识,做出战略和战术设计决策。想象一下在实践中应用这些知识会有多有趣。让我们考虑一个您正在处理新建项目的场景。您所有的同事都非常了解领域驱动设计,从一开始,所有人都在尽最大努力设计有效的模型,当然,他们都致力于使用无处不在的语言。随着项目的推进,限界上下文的边界在保护业务领域模型方面是明确且有效的。最后,由于所有战术设计决策都与业务战略保持一致,因此代码库始终处于良好状态:它使用无处不在的语言并实现适应模型复杂性的设计模式。现在醒来。
We have covered domain-driven design tools for analyzing business domains, sharing knowledge, and making strategic and tactical design decisions. Just imagine how fun it will be to apply this knowledge in practice. Let’s consider a scenario in which you are working on a greenfield project. All of your coworkers have a strong grasp of domain-driven design, and right from the get-go all are doing their best to design effective models and, of course, are devotedly using the ubiquitous language. As the project advances, the bounded contexts’ boundaries are explicit and effective in protecting the business domain models. Finally, since all tactical design decisions are aligned with the business strategy, the codebase is always in great shape: it speaks the ubiquitous language and implements the design patterns that accommodate the model’s complexity. Now wake up.
您体验我刚才描述的实验室条件的机会与中彩票一样好。当然,这是可能的,但可能性不大。不幸的是,许多人错误地认为领域驱动设计只能应用于未开发的项目以及团队中每个人都是 DDD 黑带的理想条件下。具有讽刺意味的是,最能从 DDD 中受益的项目是棕地项目:那些已经证明其业务可行性并且需要重组以对抗累积的技术债务和设计熵的项目。巧合的是,我们大部分的软件工程职业生涯都花在处理这样的棕色地带、遗留的、大泥球代码库上。
Your chances of experiencing the laboratory conditions I just described are about as good as winning the lottery. Of course, it’s possible, but not likely. Unfortunately, many people mistakenly believe that domain-driven design can only be applied in greenfield projects and in ideal conditions in which everybody on the team is a DDD black belt. Ironically, the projects that can benefit from DDD the most are the brownfield projects: those that already proved their business viability and need a shake-up to fight accumulated technical debt and design entropy. Coincidentally, working on such brownfield, legacy, big-balls-of-mud codebases is where we spend most of our software engineering careers.
另一个关于 DDD 的常见误解是,它是一个全有或全无的命题——要么应用该方法必须提供的所有工具,要么它不是领域驱动设计。这不是真的。掌握所有这些概念似乎让人不知所措,更不用说在实践中实施它们了。幸运的是,您不必应用所有的模式和实践来从领域驱动设计中获得价值。对于棕地项目尤其如此,在合理的时间范围内引入所有模式和实践实际上是不可能的。
Another common misconception about DDD is that it’s an all-or-nothing proposition—either you apply every tool the methodology has to offer, or it’s not domain-driven design. That’s not true. It might seem overwhelming to come to grips with all of these concepts, let alone implement them in practice. Luckily, you don’t have to apply all of the patterns and practices to gain value from domain-driven design. This is especially true for brownfield projects, where it’s practically impossible to introduce all the patterns and practices in a reasonable time frame.
在本章中,您将学习在现实世界中应用领域驱动设计工具和模式的策略,包括在棕地项目和不太理想的环境中。
In this chapter, you will learn strategies for applying domain-driven design tools and patterns in the real world, including on brownfield projects and in less-than-ideal environments.
按照我们探索领域驱动设计模式的顺序和实践,在组织中引入 DDD 的最佳起点是花时间了解组织的业务战略及其系统架构的当前状态。
Following the order of our exploration of domain-driven design patterns and practices, the best starting point for introducing DDD in an organization is to invest time in understanding the organization’s business strategy and the current state of its systems’ architecture.
First, identify the company’s business domain:
组织的业务领域是什么?
What is the organization’s business domain(s)?
它的客户是谁?
Who are its customers?
组织向客户提供什么服务或价值?
What service, or value, does the organization provide to customers?
该组织与哪些公司或产品竞争?
What companies or products is the organization competing with?
回答这些问题将使您对公司的高层目标有一个鸟瞰图。接下来,“放大”域并寻找组织用来实现其高级目标的业务构建块:子域。
Answering these questions will give you a bird’s-eye view of the company’s high-level goals. Next, “zoom in” to the domain and look for the business building blocks the organization employs to achieve its high-level goals: the subdomains.
一个很好的初始启发式是公司的组织结构图:它的部门和其他组织单位。检查这些部门如何合作以允许公司在其业务领域中竞争。
A good initial heuristic is the company’s org chart: its departments and other organizational units. Examine how these units cooperate to allow the company to compete in its business domain.
Furthermore, look for the signs of specific types of subdomains.
To identify the company’s core subdomains, look for what differentiates it from its competitors:
公司是否有竞争对手所缺乏的“秘方”?例如知识产权,比如内部设计的专利和算法?
Does the company have a “secret sauce” that its competitors lack? For example, intellectual property, such as patents and algorithms designed in-house?
请记住,竞争优势以及核心子域不一定是技术性的。公司是否拥有非技术竞争优势?比如,能否聘请顶级人才,产生独特的艺术设计等等?
Keep in mind that the competitive advantage, and thus the core subdomains, are not necessarily technical. Does the company possess a nontechnical competitive advantage? For example, the ability to hire top-level personnel, produce a unique artistic design, and so on?
另一个针对核心子域的强大但不幸的启发式方法是识别设计最差的软件组件——所有工程师都讨厌的那些大泥球,但由于伴随的业务风险,企业不愿从头开始重写。这里的关键是遗留系统不能用现成的系统替换——它将是一个通用的子域——并且对其进行任何修改都会带来业务风险。
Another powerful yet unfortunate heuristic for core subdomains is identifying the worst-designed software components—those big balls of mud that all engineers hate but the business is unwilling to rewrite from scratch because of the accompanying business risk. The key here is that the legacy system cannot be replaced with a ready-made system—it would be a generic subdomain—and any modification to it entails business risks.
要识别通用子域,请寻找现成的解决方案,订阅服务,或开源软件的集成。正如您在第 1 章中了解到的,相同的现成解决方案应该可供竞争公司使用,并且那些利用相同解决方案的公司应该不会对您的公司产生业务影响。
To identify generic subdomains, look for off-the-shelf solutions, subscription services, or integration of open source software. As you learned in Chapter 1, the same ready-made solutions should be available to the competing companies, and those companies leveraging the same solution should have no business impact on your company.
对于支持子域,寻找剩余的软件组件不能用现成的解决方案替代,但不能直接提供竞争优势。如果代码的形状很粗糙,它不会引起软件工程师的情绪反应,因为它很少更改。因此,次优软件设计的影响不如核心子域那么严重。
For supporting subdomains, look for the remaining software components that cannot be replaced with ready-made solutions yet do not directly provide a competitive advantage. If the code is in rough shape, it triggers less emotional response from software engineers since it changes infrequently. Thus, the effects of the suboptimal software design are not as severe as for the core subdomains.
您不必识别所有核心子域。这样做是不切实际的,甚至是不可能的,即使对于中型公司也是如此。相反,确定整体结构,但要更加关注与您正在使用的软件系统最相关的子域。
You don’t have to identify all of the core subdomains. It won’t be practical or even possible to do so, even for a medium-sized company. Instead, identify the overall structure, but pay closer attention to the subdomains that are most relevant to the software systems you are working on.
熟悉问题域后,您可以继续调查解决方案及其设计决策。首先,从高级组件开始。这些不一定是 DDD 意义上的有界上下文,而是用于将业务域分解为子系统的边界。
Once you are familiar with the problem domain, you can continue to investigate the solution and its design decisions. First, start with the high-level components. These are not necessarily bounded contexts in the DDD sense, but rather boundaries used to decompose the business domain into subsystems.
要寻找的特征属性是组件的解耦生命周期。即使子系统在同一个源代码控制存储库 (mono-repo) 中进行管理,或者如果所有组件都位于一个单一的整体代码库中,也要检查哪些可以独立于其他组件进行演化、测试和部署。
The characteristic property to look for is the components’ decoupled lifecycles. Even if the subsystems are managed in the same source control repository (mono-repo) or if all the components reside in a single monolithic codebase, check which can be evolved, tested, and deployed independently from the others.
对于每个高级组件,检查哪些业务子域它包含哪些技术设计决策?采用哪些模式来实现业务逻辑和定义组件的体系结构?
For each high-level component, check which business subdomains it contains and what technical design decisions were taken: what patterns are used to implement the business logic and define the component’s architecture?
解决方案是否适合问题的复杂性?是否存在需要更精细的设计模式的领域?相反,是否有任何子域可以偷工减料或使用现有的现成解决方案?使用此信息做出更明智的战略和战术决策。
Does the solution fit the complexity of the problem? Are there areas where more elaborate design patterns are needed? Conversely, are there any subdomains where it’s possible to cut corners or use existing, off-the-shelf solutions? Use this information to make smarter strategic and tactical decisions.
使用高级组件的知识来绘制当前的图表设计的上下文映射,就好像这些高级组件是有界上下文一样。根据限界上下文集成模式识别和跟踪组件之间的关系。
Use the knowledge of the high-level components to chart the current design’s context map, as though these high-level components were bounded contexts. Identify and track the relationships between the components in terms of bounded context integration patterns.
最后,分析生成的上下文映射并从领域驱动设计的角度评估体系结构。是否存在次优的战略设计决策?例如:
Finally, analyze the resultant context map and evaluate the architecture from a domain-driven design perspective. Are there suboptimal strategic design decisions? For example:
多个团队在同一个高级组件上工作
Multiple teams working on the same high-level component
核心子域的重复实现
Duplicate implementations of core subdomains
外包公司实施核心子域
Implementation of a core subdomain by an outsourced company
由于经常失败的集成而产生摩擦
Friction because of frequently failing integration
从外部服务和遗留系统传播的尴尬模型
Awkward models spreading from external services and legacy systems
这些见解是规划设计现代化战略的良好起点。但首先,鉴于对问题(业务领域)和解决方案(当前设计)空间的更深入了解,寻找失去领域知识。正如我们在第 11 章中讨论的那样,业务领域的知识可能会由于各种原因而丢失。该问题在核心子域中普遍存在且严重,其中业务逻辑既复杂又对业务至关重要。如果您遇到此类情况,请促进 EventStorming 会话以尝试恢复知识。此外,使用 EventStorming 会话作为培养通用语言的基础。
These insights are a good starting point for planning the design modernization strategy. But first, given this more in-depth knowledge of both the problem (business domain) and the solution (current design) spaces, look for lost domain knowledge. As we discussed in Chapter 11, knowledge of the business domain can get lost for various reasons. The problem is widespread and acute in core subdomains, where the business logic is both complex and business critical. If you encounter such cases, facilitate EventStorming sessions to try to recover the knowledge. Also, use the EventStorming session as the foundation for cultivating a ubiquitous language.
工程师们正在尝试的“大改写”努力从头开始重写系统,这次正确地设计和实施整个系统,很少能成功。管理层很少支持这种架构改造。
The “big rewrite” endeavors, in which the engineers are trying to rewrite the system from scratch, this time designing and implementing the whole system correctly, are rarely successful. Even more rarely does management support such architectural makeovers.
改进现有系统设计的一种更安全的方法是从大处着眼,从小处着手。正如 Eric Evans 所说,并非大型系统的所有部分都会设计得很好。这是我们必须接受的事实,因此我们必须从战略上决定在现代化方面投资哪里。做出此决定的先决条件是有划分系统子域的边界。边界不必是物理的,使每个子域成为一个完整的有界上下文。相反,首先要确保至少逻辑边界(命名空间、模块和包,取决于技术堆栈)与子域的边界对齐,如图 13-1所示。
A safer approach to improving the design of existing systems is to think big but start small. As Eric Evans says, not all of a large system will be well designed. That’s a fact we have to accept, and therefore we must strategically decide where to invest in terms of modernization efforts. A prerequisite for making this decision is to have boundaries dividing the system’s subdomains. The boundaries don’t have to be physical, making each subdomain a full-fledged bounded context. Instead, start by ensuring that at least the logical boundaries (namespace, modules, and packages, depending on the technology stack) are aligned with the subdomains’ boundaries, as shown in Figure 13-1.
调整系统模块是一种相对安全的重构形式。您不是在修改业务逻辑,只是将类型重新定位到组织更完善的结构中。也就是说,确保完整类型名称的引用(例如库的动态加载、反射等)不会中断。
Adjusting the system’s modules is a relatively safe form of refactoring. You are not modifying the business logic, just repositioning the types in a more well-organized structure. That said, ensure that references by full type names, such as the dynamic loading of libraries, reflection, and so on, are not breaking.
此外,跟踪子域在不同代码库中实现的业务逻辑;数据库中的存储过程、无服务器函数等。确保也在这些平台中引入新的边界。例如,如果某些逻辑在数据库的存储过程中处理,则要么重命名过程以反映它们所属的模块,要么引入专用的数据库模式并重新定位存储过程。
In addition, keep track of the subdomains’ business logic implemented in different codebases; stored procedures in a database, serverless functions, and so on. Make sure to introduce the new boundaries in those platforms as well. For instance, if some of the logic is handled in the database’s stored procedures, either rename the procedures to reflect the module they belong to or introduce a dedicated database schema and relocate the stored procedures.
正如我们在第 10 章中讨论的那样,这可能是有风险的过早地将系统分解为尽可能小的有界上下文。我们将在下一章更详细地讨论限界上下文和微服务。现在,通过将逻辑边界转变为物理边界,寻找可以在何处获得最大价值。通过将逻辑边界转换为物理边界来提取有界上下文的过程如图13-2所示。
As we discussed in Chapter 10, it can be risky to prematurely decompose the system into the smallest bounded contexts possible. We will discuss bounded contexts and microservices in more detail in the next chapter. For now, look for where the most value can be gained by turning the logical boundaries into physical boundaries. The process of extracting a bounded context(s) by turning a logical boundary into a physical one is shown in Figure 13-2.
问自己的问题:
Questions to ask yourself:
是否有多个团队在同一个代码库上工作?如果是这样,通过为每个团队定义限界上下文来解耦开发生命周期。
Are multiple teams working on the same codebase? If so, decouple the development lifecycles by defining bounded contexts for each team.
不同组件是否使用相互冲突的模型?如果是这样,将冲突的模型重新定位到单独的有界上下文中。
Are conflicting models being used by the different components? If so, relocate the conflicting models into separate bounded contexts.
当最低要求的有界上下文到位时,检查它们之间的关系和集成模式。查看处理不同限界上下文的团队如何沟通和协作。特别是当他们通过临时或类似共享内核的集成进行通信时,团队是否有共同的目标和足够的协作水平?
When the minimum required bounded contexts are in place, examine the relationships and integration patterns between them. See how the teams working on different bounded contexts communicate and collaborate. Especially when they are communicating through ad hoc or shared-kernel–like integration, do the teams have shared goals and adequate collaboration levels?
注意上下文集成模式可以解决的问题:
Pay attention to problems that the context integration patterns can address:
首先,从战术的角度来看,寻找最“痛苦”的错位在业务价值和实施策略方面,例如核心子域实施与模型复杂性不匹配的模式——事务脚本或活动记录。这些直接影响业务成功的系统组件必须经常更改,但由于设计不佳,维护和发展起来很痛苦。
First and foremost, from a tactical standpoint, look for the most “painful” mismatches in business value and implementation strategies, such as core subdomains implementing patterns that don’t match the complexity of the model—transaction script or active record. These system components that directly impact the success of the business have to change the most often, yet are painful to maintain and evolve due to poor design.
成功实现设计现代化的先决条件是领域知识和业务领域的有效模型。正如我在本书中多次提到的,领域驱动设计的无处不在的语言对于获取知识和构建有效的解决方案模型至关重要。
A prerequisite to the successful modernization of a design is the domain knowledge and effective model of the business domain. As I have mentioned several times throughout this book, domain-driven design’s ubiquitous language is essential for achieving knowledge and building an effective solution model.
不要忘记领域驱动设计收集领域知识的捷径:EventStorming。使用 EventStorming 与领域专家一起构建一种无处不在的语言并探索遗留代码库,尤其是当代码库是没有人真正理解的未记录的混乱时。聚集与其功能相关的每个人并探索业务领域。EventStorming 是恢复领域知识的绝佳工具。
Don’t forget domain-driven design’s shortcut for gathering domain knowledge: EventStorming. Use EventStorming to build a ubiquitous language with the domain experts and explore the legacy codebase, especially if the codebase is an undocumented mess that no one truly understands. Gather everyone related to its functionality and explore the business domain. EventStorming is a fantastic tool for recovering domain knowledge.
一旦您掌握了领域知识及其模型,就可以决定哪种业务逻辑实现模式最适合所讨论的业务功能。作为起点,使用第 10 章中描述的设计试探法。您必须做出的下一个决定涉及现代化策略:逐步替换系统的整个组件(扼杀者模式),或逐步重构现有解决方案。
Once you are equipped with the domain knowledge and its model(s), decide which business logic implementation patterns best suit the business functionality in question. As a starting point, use the design heuristics described in Chapter 10. The next decision you have to make concerns the modernization strategy: gradually replacing whole components of the system (the strangler pattern), or gradually refactoring the existing solution.
Strangler fig ,如图 13-3所示,是一个具有特殊生长模式的热带树科:Strangler fig 生长在其他树木(寄主树)之上。扼杀者的生命始于寄主树上部树枝上的种子。随着扼杀者的成长,它会向下移动直到在土壤中扎根。最终,扼杀者会长出遮蔽寄主树的叶子,导致寄主树死亡。
Strangler fig, shown in Figure 13-3, is a family of tropical trees that share a peculiar growth pattern: stranglers grow over other trees—host trees. A strangler begins its life as a seed in the upper branches of the host tree. As the strangler grows, it makes its way down until it roots in the soil. Eventually, the strangler grows foliage that overshadows the host tree, leading to the host tree’s death.
扼杀者迁移模式基于与该模式命名的树相同的生长动力。这个想法是创建一个新的限界上下文——扼杀者——用它来实现新的需求,并逐渐将遗留上下文的功能迁移到其中。同时,除了修补程序和其他紧急情况外,遗留限界上下文的演化和发展停止。最终,所有功能都迁移到新的限界上下文——扼杀者——并按照类推,导致宿主——遗留代码库的死亡。
The strangler migration pattern is based on the same growth dynamic as the tree the pattern is named after. The idea is to create a new bounded context—the strangler—use it to implement new requirements, and gradually migrate the legacy context’s functionality into it. At the same time, except for hotfixes and other emergencies, the evolution and development of the legacy bounded context stops. Eventually, all functionality is migrated to the new bounded context—the strangler— and following the analogy, leading to the death of the host—the legacy codebase.
通常,strangler 模式与 façade 模式一起使用:一个薄的抽象层,充当公共接口,负责将请求转发给遗留或现代化的有界上下文进行处理。当迁移完成时——也就是说,当主机死亡时——façade 被移除,因为它不再是必需的(见图13-4)。
Usually, the strangler pattern is used in tandem with the façade pattern: a thin abstraction layer that acts as the public interface and is in charge of forwarding the requests to processing either by the legacy or the modernized bounded context. When migration completes—that is, when the host dies—the façade is removed as it is no longer necessary (see Figure 13-4).
与每个限界上下文都是一个单独的子系统,因此不能与其他限界上下文共享其数据库的原则相反,在实现扼杀者模式时可以放宽规则。为了避免上下文之间的复杂集成,现代化上下文和遗留上下文都可以使用相同的数据库,这在许多情况下可能需要分布式事务——两个上下文必须使用相同的数据,如图 13-5所示。
Contrary to the principle that each bounded context is a separate subsystem, and thus cannot share its database with other bounded contexts, the rule can be relaxed when implementing the strangler pattern. Both the modernized and the legacy contexts can use the same database for the sake of avoiding complex integration between the contexts, which in many cases can entail distributed transactions—both contexts have to work with the same data, as shown in Figure 13-5.
改变每个限界上下文一个数据库规则的条件是,最终,而且越早越好,遗留上下文将被淘汰,并且数据库将由新实现独占使用。
The condition for bending the one-database-per-bounded-context rule is that eventually, and better sooner than later, the legacy context will be retired, and the database will be used exclusively by the new implementation.
基于扼杀器的迁移的替代方法是对遗留代码库进行现代化改造,也称为重构。
An alternative to strangler-based migration is modernizing the legacy codebase in place, also called refactoring.
在第 11 章中,您了解了迁移战术设计决策的各个方面。但是,在对遗留代码库进行现代化改造时需要注意两个细微差别。
In Chapter 11, you learned the various aspects of migrating tactical design decisions. However, there are two nuances to be aware of when modernizing a legacy codebase.
首先,小的增量步骤比大的重写更安全。因此,不要将事务脚本或活动记录直接重构为事件源域模型。相反,采取设计基于状态的聚合的中间步骤。努力寻找有效的聚合边界。确保所有相关的业务逻辑都在这些边界内。从基于状态的聚合到事件源聚合比在事件源聚合中发现错误的事务边界要安全几个数量级。
First, small incremental steps are safer than a big rewrite. Therefore, don’t refactor a transaction script or active record straight to an event-sourced domain model. Instead, take the intermediate step of designing state-based aggregates. Invest the effort in finding effective aggregate boundaries. Ensure that all related business logic resides within those boundaries. Going from state-based to event-sourced aggregates will be orders of magnitude safer than discovering wrong transactional boundaries in an event-sourced aggregate.
其次,遵循采取小的增量步骤的相同推理,重构为域模型不一定是原子更改。相反,您可以逐渐引入域模型模式的元素。
Second, following the same reasoning of taking small incremental steps, refactoring to a domain model doesn’t have to be an atomic change. Instead, you can gradually introduce the elements of the domain model pattern.
首先寻找可能的值对象。不可变对象可以显着降低解决方案的复杂性,即使您没有使用成熟的领域模型。
Start by looking for possible value objects. Immutable objects can significantly reduce the solution’s complexity, even if you are not using a full-blown domain model.
正如我们在第 11 章中讨论的那样,将活动记录重构为聚合并非一蹴而就。它可以循序渐进地进行。首先收集相关的业务逻辑。接下来,分析事务边界。是否存在需要强一致性但对最终一致性数据进行操作的决策?或者相反,解决方案是否在最终一致性就足够的情况下强制执行强一致性?在分析代码库时,不要忘记这些决策是由业务而不是技术问题驱动的。只有在对事务需求进行全面分析之后,您才应该设计聚合的边界。
As we discussed in Chapter 11, refactoring active records into aggregates doesn’t have to be done overnight. It can be done in gradual steps. Start by gathering the related business logic. Next, analyze the transactional boundaries. Are there decisions that require strong consistency but operate on eventually consistent data? Or conversely, does the solution enforce strong consistency where eventual consistency would suffice? When analyzing the codebase, don’t forget that these decisions are driven by business, not technology, concerns. Only after a thorough analysis of the transactional requirements should you design the aggregate’s boundaries.
最后,在重构遗留系统时必要时,使用反腐败层保护新代码库免受旧模型的影响,并通过实施开放主机服务和公开已发布的语言来保护消费者免受遗留代码库更改的影响。
Finally, when necessary as you’re refactoring legacy systems, protect the new codebase from old models using an anticorruption layer, and protect the consumers from changes in the legacy codebase by implementing an open-host service and exposing a published language.
正如我们在本章的介绍中所讨论的,应用领域驱动设计不是全有或全无的努力。您不必应用 DDD 必须提供的所有工具。例如,出于某种原因,战术模式可能不适合您。也许您更喜欢使用其他设计模式,因为它们在您的特定领域工作得更好,或者只是因为您发现其他模式更有效。那完全没问题!
As we discussed in this chapter’s introduction, applying domain-driven design is not an all-or-nothing endeavor. You don’t have to apply every tool DDD has to offer. For example, for some reason, the tactical patterns might not work for you. Maybe you prefer to use other design patterns because they work better in your specific domain, or just because you find other patterns more effective. That’s totally fine!
只要你分析你的业务领域和它的战略,寻找有效的模型来解决特定的问题,最重要的是,根据业务领域的需求做出设计决策:这就是领域驱动设计!
As long as you analyze your business domain and its strategy, look for effective models to solve particular problems, and most importantly, make design decisions based on the business domain’s needs: that’s domain-driven design!
值得重申的是,领域驱动设计与聚合或值对象无关。领域驱动设计是关于让您的业务领域驱动软件设计决策。
It’s worth reiterating that domain-driven design is not about aggregates or value objects. Domain-driven design is about letting your business domain drive software design decisions.
当我在技术会议上介绍这个话题时,有一个问题几乎每次我都会被问到:“这听起来不错,但我如何向我的团队和管理层‘推销’领域驱动设计?” 这是一个极其重要的问题。
When I present on this topic at technology conferences, there is one question that I’m asked almost every time: “That all sounds great, but how do I ‘sell’ domain-driven design to my team and management?” That’s an extremely important question.
销售很难,就我个人而言,我讨厌销售。也就是说,如果您考虑一下,设计软件就是销售。我们向团队、管理层或客户推销我们的想法。然而,涵盖如此广泛的设计决策方面,甚至超出工程领域以涉及其他利益相关者的方法可能极难推销。
Selling is hard, and personally, I hate selling. That said, if you think about it, designing software is selling. We are selling our ideas to the team, to management, or to customers. However, a methodology that covers such a wide range of design decision aspects, and even reaches outside the engineering zone to involve other stakeholders, can be extremely hard to sell.
管理支持对于在组织中进行任何重大变革都是必不可少的。然而,除非高层管理人员已经熟悉领域驱动设计或愿意花时间学习该方法的商业价值,否则这对他们来说并不是最重要的,尤其是在工程流程看似发生重大转变的情况下DDD 需要。然而,幸运的是,这并不意味着您不能使用领域驱动设计。
Management support is essential for making any considerable changes in an organization. However, unless the top-level managers are already familiar with domain-driven design or are willing to invest time to learn the business value of the methodology, it’s not top of mind for them, especially because of a seemingly large shift in the engineering process that DDD entails. Fortunately, however, it doesn’t mean you can’t use domain-driven design.
让领域驱动设计成为你专业工具箱的一部分,不是组织战略。DDD 的模式和实践是工程技术,既然软件工程是你的工作,那就使用它们吧!
Make domain-driven design a part of your professional toolbox, not an organizational strategy. DDD’s patterns and practices are engineering techniques, and since software engineering is your job, use them!
让我们看看如何将 DDD 融入到你的日常工作中,而不用花太多功夫。
Let’s see how to incorporate DDD into your day-to-day job without making much ado about it.
使用通用语言是领域驱动设计的基石实践。它对于领域知识发现、交流和有效的解决方案建模至关重要。
The use of a ubiquitous language is the cornerstone practice of domain-driven design. It is essential for domain knowledge discovery, communication, and effective solution modeling.
幸运的是,这种做法是如此微不足道,以至于它是临界常识。仔细聆听利益相关者在谈论业务领域时使用的语言。温和地将术语从技术术语转向其业务含义。
Luckily, this practice is so trivial that it’s borderline common sense. Listen carefully to the language the stakeholders use when they speak about the business domain. Gently steer the terminology away from technical jargon and toward its business meaning.
寻找不一致的术语并要求澄清。例如,如果同一事物有多个名称,请查找原因。这些不同的模型是否交织在同一个解决方案中?寻找上下文并使它们明确。如果含义相同,请遵循常识并要求使用一个术语。
Look for inconsistent terms and ask for clarifications. For example, if there are multiple names for the same thing, look for the reason. Are those different models intertwined in the same solution? Look for contexts and make them explicit. If the meaning is the same, follow common sense and ask for one term to be used.
另外,尽可能与领域专家交流。这些努力不一定需要正式会议。饮水机和茶歇是很好的沟通促进者。与领域专家讨论业务领域。尝试使用他们的语言。寻找理解上的困难并要求澄清。别担心——领域专家通常很乐意与真正有兴趣了解问题领域的工程师合作!
Also, communicate with domain experts as much as possible. These efforts shouldn’t necessarily require formal meetings. Watercoolers and coffee breaks are great communication facilitators. Speak with the domain experts about the business domain. Try using their language. Look for difficulties in understanding and ask for clarifications. Don’t worry—domain experts are usually happy to collaborate with engineers who are sincerely interested in learning about the problem domain!
最重要的是,在您的代码和所有与项目相关的通信中使用通用语言。要有耐心。更改已在组织中使用了一段时间的术语需要时间,但最终会流行起来。
Most importantly, use the ubiquitous language in your code and all project-related communication. Be patient. Changing the terminology that has been used in an organization for a while will take time, but eventually, it will catch on.
When exploring possible decomposition options, resolve to the principles behind what the bounded context pattern is based on:
为什么为所有用例设计面向问题的模型而不是单一模型更好?因为“一体式”解决方案很少对任何事情都有效。
Why is it better to design problem-oriented models instead of a single model for all use cases? Because “all-in-one” solutions are rarely effective for anything.
为什么限界上下文不能承载冲突的模型?因为增加了认知负荷和解决方案的复杂性。
Why can’t a bounded context host conflicting models? Because of the increased cognitive load and solution complexity.
为什么多个团队在同一个代码库上工作是个坏主意?由于团队之间的摩擦和协作受阻。
Why is it a bad idea for multiple teams to work on the same codebase? Because of friction and hindered collaboration between the teams.
对限界上下文集成模式使用相同的推理:确保您了解每个模式应该解决的问题。
Use the same reasoning for bounded context integration patterns: make sure you understand the problem each pattern is supposed to solve.
在讨论战术设计模式时,不要诉诸于权威:“让我们在这里使用聚合,因为 DDD 书是这么说的!” 相反,诉诸逻辑。例如:
When discussing tactical design patterns, don’t appeal to authority: “Let’s use an aggregate here because the DDD book says so!” Instead, appeal to logic. For example:
为什么明确的交易边界很重要?以保护数据的一致性。
Why are explicit transactional boundaries important? To protect the consistency of the data.
为什么一个数据库事务不能修改聚合的多个实例?以确保一致性边界是正确的。
Why can’t a database transaction modify more than one instance of an aggregate? To ensure that the consistency boundaries are correct.
为什么聚合的状态不能由外部组件直接修改?确保所有相关的业务逻辑都位于同一位置而不是重复的。
Why can’t an aggregate’s state be modified directly by an external component? To ensure that all the related business logic is colocated and not duplicated.
为什么我们不能将聚合的某些功能卸载到存储过程中?确保没有重复的逻辑。重复的逻辑,尤其是在系统的逻辑和物理距离较远的组件中,往往会不同步并导致数据损坏。
Why can’t we offload some of the aggregate’s functionality to a stored procedure? To make sure that no logic is duplicated. Duplicated logic, especially in logically and physically distant components of a system, tends to go out of sync and lead to data corruption.
为什么我们要争取小的聚合边界?因为广泛的事务范围既会增加聚合的复杂性,也会对性能产生负面影响。
Why should we strive for small aggregate boundaries? Because wide transactional scope will both increase the complexity of the aggregate and negatively impact the performance.
为什么我们不能直接将事件写入日志文件而不是事件源?因为没有长期的数据一致性保证。
Why, instead of event sourcing, can’t we just write events to a logfile? Because there are no long-term data consistency guarantees.
说到事件溯源,当解决方案需要事件溯源领域模型时,这种模式的实现可能很难推销。让我们来看看可以帮助解决这个问题的绝地思维技巧。
Speaking of event sourcing, when the solution calls for an event-sourced domain model, implementation of this pattern might be hard to sell. Let’s take a look at a Jedi mind trick that can help with this.
尽管有许多优点,但事件溯源对许多人来说听起来过于激进。与我们在本书中讨论的所有内容一样,解决方案是让业务领域来推动这一决策。
Despite its many advantages, event sourcing sounds too radical for many people. As with everything we’ve discussed in this book, the solution is to let the business domain drive this decision.
与领域专家交谈。向他们展示基于状态和事件的模型。解释事件溯源的差异和优势,尤其是在时间维度方面。通常情况下,他们会对它提供的洞察力水平感到欣喜若狂,并且会自己提倡事件溯源。
Talk to domain experts. Show them the state- and event-based models. Explain the differences and the advantages offered by event sourcing, especially with regard to the dimension of time. More often than not, they will be ecstatic with the level of insight it provides and will advocate event sourcing themselves.
And while interacting with the domain experts, don’t forget to work on the ubiquitous language!
在本章中,您学习了在现实场景中利用领域驱动设计工具的各种技术:在处理棕地项目和遗留代码库时,不一定与 DDD 专家团队一起工作。
In this chapter, you learned various techniques for leveraging domain-driven design tools in real-life scenarios: when working on brownfield projects and legacy codebases, and not necessarily with a team of DDD experts.
与新建项目一样,始终从分析业务领域开始。公司的目标和实现这些目标的策略是什么?使用组织结构和现有软件设计决策来识别组织的子域及其类型。有了这些知识,就可以规划现代化战略。寻找痛点。寻求获得最大的商业价值。通过重构或替换相关组件使遗留代码现代化。不管怎样,循序渐进。大的重写带来的风险大于商业价值!
As in greenfield projects, always start by analyzing the business domain. What are the company’s goals and its strategy for achieving them? Use the organizational structure and existing software design decisions to identify the organization’s subdomains and their types. With this knowledge, plan the modernization strategy. Look for pain points. Look to gain the most business value. Modernize legacy code either by refactoring or by replacing the relevant components. Either way, do it gradually. Big rewrites entail more risk than business value!
最后,即使 DDD 在你的组织中没有被广泛采用,你也可以使用领域驱动设计工具。使用正确的工具,并在与同事讨论它们时,始终使用每种模式背后的逻辑和原则。
Finally, you can use domain-driven design tools even if DDD is not widely adopted in your organization. Use the right tools, and when discussing them with colleagues, always use the logic and principles behind each pattern.
本章结束了我们对领域驱动设计的讨论。在第 IV 部分中,您将了解 DDD 与其他方法和模式的相互作用。
This chapter concludes our discussion of domain-driven design on its own. In Part IV, you will learn about the interplay of DDD with other methodologies and patterns.
假设您想将领域驱动的设计工具和实践引入到一个棕地项目中。你的第一步是什么?
将所有业务逻辑重构为事件源域模型。
分析组织的业务领域及其战略。
通过确保它们遵循适当的有界上下文的原则来改进系统的组件。
在棕地项目中不可能使用领域驱动设计。
Assume you want to introduce domain-driven design tools and practices to a brownfield project. What is going to be your first step?
Refactor all business logic to the event-sourced domain model.
Analyze the organization’s business domain and its strategy.
Improve the system’s components by ensuring that they follow the principles of proper bounded contexts.
It’s impossible to use domain-driven design in a brownfield project.
在迁移过程中,扼杀者模式在哪些方面与领域驱动设计的一些核心原则相矛盾?
多个限界上下文正在使用共享数据库。
如果现代化的限界上下文是一个核心子域,它的实现会在旧的和新的实现中重复。
多个团队在同一个限界上下文中工作。
A和B。
In what ways does the strangler pattern contradict some of the core principles of domain-driven design during the migration process?
Multiple bounded contexts are using a shared database.
If the modernized bounded context is a core subdomain, its implementation gets duplicated in the old and the new implementations.
Multiple teams are working on the same bounded context.
A and B.
为什么将基于活动记录的业务逻辑直接重构到事件源域模型通常不是一个好主意?
基于状态的模型可以更轻松地在学习过程中重构聚合的边界。
逐步引入大的变化更安全。
A和B。
以上都不是。甚至将事务脚本直接重构为事件源域模型也是合理的。
Why is it generally not a good idea to refactor active-record-based business logic straight into the event-sourced domain model?
A state-based model makes it easier to refactor aggregates’ boundaries during the learning process.
It’s safer to introduce big changes gradually.
A and B.
None of the above. It’s reasonable to refactor even a transaction script straight into an event-sourced domain model.
当您引入聚合模式时,您的团队会问为什么聚合不能只引用所有可能的实体,从而使从一个地方遍历整个业务领域成为可能。你如何回答他们?
When you’re introducing the aggregate pattern, your team asks why the aggregate can’t just reference all the possible entities and thus make it possible to traverse the whole business domain from one place. How do you answer them?
到目前为止,您已经在本书中学习了如何根据组织的业务战略和需求使用领域驱动设计来设计软件解决方案。我们看到了如何应用 DDD 工具和实践来理解业务领域、设计系统组件的边界以及实现业务逻辑。
So far in this book you’ve learned how to use domain-driven design to design software solutions according to an organization’s business strategy and needs. We saw how to apply DDD tools and practices to make sense of the business domain, design the boundaries of the system’s components, and implement the business logic.
领域驱动设计涵盖了软件开发生命周期的大部分内容,但不能涵盖所有的软件工程。其他方法和工具各有其作用。在第四部分,我们将讨论 DDD 与其他方法和模式的关系:
Domain-driven design covers a lot of the software development lifecycle, but it can’t cover all of software engineering. Other methodologies and tools have their roles. In Part IV, we will discuss DDD in relation to other methodologies and patterns:
由于基于微服务的架构风格的流行,领域驱动设计获得了大部分吸引力,这已经不是什么秘密了。在第 14 章中,我们将探讨微服务和领域驱动设计之间的相互作用,以及这两种方法如何相互补充。
It’s no secret that domain-driven design gained most of its traction due to the popularity of the microservices-based architectural style. In Chapter 14, we will explore the interplay between microservices and domain-driven design and how the two approaches complement each other.
事件驱动架构是构建可扩展、高性能和弹性分布式系统的一种流行方法。在第 15 章中,您将了解事件驱动架构的原则以及如何利用 DDD 设计有效的异步通信。
The event-driven architecture is a popular method of architecting scalable, performant, and resilient distributed systems. In Chapter 15, you will learn the principles of event-driven architecture and how to leverage DDD to design effective asynchronous communication.
第 16 章以数据分析背景下的有效建模作为本书的结尾。您将了解主要的数据管理架构、数据仓库和数据湖,以及数据网格架构如何解决它们的缺点。我们还将分析和讨论 DDD 和数据网格架构如何基于相同的设计原则和目标。
Chapter 16 concludes the book with effective modeling in the context of data analytics. You will learn about the predominant data management architectures, data warehouses and data lakes, and how their shortcomings are addressed by the data mesh architecture. We will also analyze and discuss how DDD and the data mesh architecture are based on the same design principles and goals.
在 2010 年代中期,微服务席卷了软件工程行业。其目的是满足现代系统快速变化、扩展和自然地适应云计算的分布式特性的需求。许多公司做出了分解其单一代码库的战略决策,以支持基于微服务的架构所提供的灵活性。不幸的是,许多这样的努力都没有好的结果。这些公司没有采用灵活的架构,而是最终得到了分布式的大泥球——这些设计比公司想要拆分的单体设计更脆弱、更笨拙、更昂贵。
In the mid-2010s, microservices took the software engineering industry by storm. The intent was to address modern systems’ need to change quickly, scale, and fit the distributed nature of cloud computing naturally. Many companies made the strategic decision to decompose their monolithic codebases in favor of the flexibility provided by the microservices-based architecture. Unfortunately, many such endeavors didn’t end well. Instead of flexible architectures, these companies ended up with distributed big balls of mud—designs that are much more fragile, clumpy, and expensive than the monoliths the companies wanted to break apart.
从历史上看,微服务通常与 DDD 相关联,尤其是与限界上下文模式相关联。许多人甚至可以互换使用术语限界上下文和微服务。但它们真的是一回事吗?本章探讨领域驱动设计方法与微服务架构模式之间的关系。您将了解模式之间的相互作用,更重要的是,您将了解如何利用 DDD 设计有效的基于微服务的系统。
Historically, microservices are often associated with DDD, especially with the bounded context pattern. Many people even use the terms bounded context and microservices interchangeably. But are they really the same thing? This chapter explores the relationship between domain-driven design methodology and the microservices architectural pattern. You will learn the interplay between the patterns, and more importantly, how you can leverage DDD to design effective microservices-based systems.
让我们从基础开始,定义什么是服务和微服务。
Let’s start with the basics and define what exactly are services and microservices.
根据 OASIS,服务是一种允许访问的机制一个或多个功能,其中访问是使用规定的接口提供的。1规定的接口是将数据输入或输出服务的任何机制。它可以是同步的,例如请求/响应模型,也可以是异步的,例如生成和使用事件的模型。这是服务公共接口,如图14-1所示,它提供了与其他系统组件通信和集成的手段。
According to OASIS, a service is a mechanism that enables access to one or more capabilities, where the access is provided using a prescribed interface.1 The prescribed interface is any mechanism for getting data in or out of a service. It can be synchronous, such as a request/response model, or asynchronous, such as a model that is producing and consuming events. This is the service public interface, as shown in Figure 14-1, which provides a means for communicating and integrating with other system components.
Randy Shoup将服务的界面比作前门。所有进出服务的数据都必须通过前门。此外,服务的公共接口定义了服务本身:服务公开的功能。一个表达良好的接口足以描述服务实现的功能。例如,图 14-2中所示的公共接口明确描述了服务的功能。
Randy Shoup likens a service’s interface to a front door. All data going into or out of the service has to pass through the front door. Furthermore, a service’s public interface defines the service itself: the functionality exposed by the service. A well-expressed interface is enough to describe the functionality implemented by a service. For example, the public interface illustrated in Figure 14-2 explicitly describes the functionality of the service.
这将我们带入微服务的定义。
This takes us to the definition of microservice.
微服务的定义非常简单。自服务由其公共接口定义,微服务是具有微公共接口的服务:微前门。
The definition of a microservice is surprisingly simple. Since a service is defined by its public interface, a microservice is a service with a micro-public interface: a micro-front door.
拥有一个微公共接口可以更容易地理解单个服务的功能及其与其他系统组件的集成。减少服务的功能也会限制其更改的原因,并使服务在开发、管理和扩展方面更加自主。
Having a micro-public interface makes it easier to understand both the function of a single service and its integration with other system components. Reducing a service’s functionality also limits its reasons for change and makes the service more autonomous for development, management, and scale.
此外,它还解释了微服务不公开其数据库的做法。公开数据库,使其成为服务前门的一部分,会使它的公共接口变得庞大。例如,您可以在关系数据库上执行多少种不同的 SQL 查询?由于 SQL 是一种非常灵活的语言,因此可能的估计是无穷大。因此,微服务封装了它们的数据库。只能通过更紧凑、面向集成的公共接口访问数据。
In addition, it explains the practice of microservices not exposing their databases. Exposing a database, making it a part of the service’s front door, would make its public interface huge. For example, how many different SQL queries can you execute on a relational database? Since SQL is quite a flexible language, the likely estimate would be infinity. Hence, microservices encapsulate their databases. The data can only be accessed through a much more compact, integration-oriented public interface.
说微服务是微公共接口看似简单。听起来好像将服务接口限制为单一方法会产生完美的微服务。让我们看看如果我们在实践中应用这种天真的分解会发生什么。
Saying that a microservice is a micro-public interface is deceptively simple. It may sound as though limiting service interfaces to a single method would result in perfect microservices. Let’s see what will happen if we apply this naïve decomposition in practice.
考虑图 14-3中的积压管理服务。它的公共接口由八个公共方法组成,我们希望应用“每个服务一个方法”的规则。
Consider the backlog management service in Figure 14-3. Its public interface consists of eight public methods, and we want to apply the “one method per service” rule.
由于这些都是行为良好的微服务,因此每个微服务都封装了自己的数据库。任何一个服务都不允许直接访问另一个服务的数据库;仅通过其公共接口。但目前,没有公共接口。这些服务必须协同工作并同步每个服务正在应用的更改。因此,我们需要扩展服务的接口来解决这些与集成相关的问题。此外,在可视化时,结果服务之间的集成和数据流类似于典型的分布式大泥球,如图14-4所示。
Since these are well-behaved microservices, each encapsulates its database. No one service is allowed to access another service’s database directly; only through its public interface. But currently, there is no public interface for that. The services have to work together and synchronize the changes each service is applying. As a result, we need to expand the services’ interfaces to account for these integration-related concerns. Furthermore, when visualized, the integrations and data flow between the resultant services resemble a typical distributed big ball of mud, as shown in Figure 14-4.
套用 Randy Shoup 的比喻,通过将系统分解为如此细粒度的服务,我们确实最小化了服务的前门。然而,为了实现总体系统的功能,我们必须为每项服务添加巨大的“仅限员工”入口。让我们看看我们可以从这个例子中学到什么。
Paraphrasing Randy Shoup’s metaphor, by decomposing the system to such fine-grained services, we definitely minimized the services’ front doors. However, to implement the overarching system’s functionality, we had to add enormous “staff only” entrances to each service. Let’s see what we can learn from this example.
遵循拥有每项服务的简单分解启发式由于多种原因,仅公开一种方法被证明是次优的。首先,这根本不可能。由于服务必须协同工作,我们被迫使用与集成相关的公共方法扩展它们的公共接口。第二,我们打赢了这场仗,输掉了这场战争。每项服务最终都比最初的设计简单得多,但最终的系统却变得复杂了几个数量级。
Following the simplistic decomposition heuristic of having each service expose only a single method proved to be suboptimal for many reasons. First, it’s simply not possible. Since the services have to work together, we were forced to expand their public interfaces with integration-related public methods. Second, we won the battle but lost the war. Each service ended up being much simpler than the original design, however the resultant system became orders of magnitude more complex.
微服务架构的目标是产生一个灵活的系统。将设计工作集中在单个组件上,但忽略它与系统其余部分的交互,有悖于非常定义一个系统:
The goal of the microservices architecture is to produce a flexible system. Concentrating the design efforts on a single component, but ignoring its interactions with the rest of the system, goes against the very definition of a system:
一组一起运行的连接事物或设备
A set of connected things or devices that operate together
为特定目的一起使用的一组计算机设备和程序
A set of computer equipment and programs used together for a particular purpose
因此,系统不能由独立的组件构建。在一个适当的基于微服务的系统中,无论多么分离,服务仍然必须集成并相互通信。让我们来看看单个微服务的复杂性和总体系统的复杂性之间的相互作用。
Hence, a system cannot be built out of independent components. In a proper microservices-based system, however decoupled, the services still have to be integrated and communicate with each other. Let’s take a look at the interplay between the complexity of individual microservices and the complexity of the overarching system.
四十年前,没有云计算,没有全球规模的需求,而且不需要每 11.7 秒部署一个系统。但是工程师们仍然必须控制系统的复杂性。尽管当时的工具有所不同,但挑战——更重要的是解决方案——在当今是相关的,并且可以应用于基于微服务的系统的设计。
Forty years ago, there was no cloud computing, there were no global-scale requirements, and there was no need to deploy a system every 11.7 seconds. But engineers still had to tame systems’ complexity. Even though the tools in those days were different, the challenges—and more importantly, the solution—are relevant nowadays and can be applied to the design of microservices-based systems.
在他的书Composite/Structured Design中,Glenford J. Myers讨论如何构造过程代码以降低其复杂性。在书的第一页,他写道:
In his book, Composite/Structured Design, Glenford J. Myers discusses how to structure procedural code to reduce its complexity. On the first page of the book, he writes:
复杂性的主题远不止试图最小化程序每个部分的局部复杂性。一种更为重要的复杂性类型是全局复杂性:程序或系统整体结构的复杂性(即程序主要部分之间的关联或相互依赖程度)。
There is much more to the subject of complexity than simply attempting to minimize the local complexity of each part of a program. A much more important type of complexity is global complexity: the complexity of the overall structure of a program or system (i.e., the degree of association or interdependence among the major pieces of a program).
在我们的上下文中,局部复杂性就是复杂性每个单独的微服务的复杂性,而全局复杂性是整个系统的复杂性。局部复杂性取决于服务的实现;全局复杂性由服务之间的交互和依赖性定义。在设计基于微服务的系统时,哪个复杂性更需要优化?让我们来分析一下这两个极端。
In our context, local complexity is the complexity of each individual microservice, whereas global complexity is the complexity of the whole system. Local complexity depends on the implementation of a service; global complexity is defined by the interactions and dependencies between the services. Which of the complexities is more important to optimize when designing a microservices-based system? Let’s analyze both extremes.
将全局复杂性降至最低非常容易。我们所要做的就是消除系统组件之间的任何交互——也就是说,在一个整体服务中实现所有功能。正如我们之前看到的,这种策略可能在某些情况下有效。在其他情况下,它可能会导致可怕的大泥球:可能是局部复杂性的最高水平。
It’s surprisingly easy to reduce global complexity to a minimum. All we have to do is eliminate any interactions between the system’s components—that is, implement all functionality in one monolithic service. As we’ve seen earlier, this strategy may work in certain scenarios. In others, it may lead to the dreaded big ball of mud: probably the highest possible level of local complexity.
另一方面,我们知道当我们只优化局部复杂性而忽略系统的全局复杂性时会发生什么——更可怕的分布式大泥球。这种关系如图 14-5所示。
On the other hand, we know what happens when we optimize only the local complexity but neglect the system’s global complexity—the even more dreaded distributed big ball of mud. This relationship is shown in Figure 14-5.
要设计一个合适的基于微服务的系统,我们必须优化全局和局部的复杂性。设定单独优化其中任何一个的设计目标是局部最优。全局最优平衡了这两种复杂性。让我们看看微公共接口的概念如何有助于平衡全局和本地的复杂性。
To design a proper microservices-based system, we have to optimize both global and local complexities. Setting the design goal of optimizing either one individually is a local optima. The global optima balances both complexities. Let’s see how the notion of micro-public interfaces lends itself to balancing global and local complexities.
就此而言,软件系统或任何系统中的模块由其功能和逻辑定义。功能是模块应该做的——它的业务功能。逻辑是模块的业务逻辑——模块如何实现其业务功能。
A module in a software system, or any system, for that matter, is defined by its function and logic. A function is what the module is supposed to do—its business functionality. The logic is the module’s business logic—how the module implements its business functionality.
John Ousterhout在他的著作《软件设计哲学》中讨论了模块化的概念,并提出了一种简单而强大的视觉启发式方法来评估模块的设计:深度。
In his book, The Philosophy of Software Design, John Ousterhout discusses the notion of modularity and proposes a simple yet powerful visual heuristic for evaluating a module’s design: depth.
Ousterhout 建议将模块可视化为矩形,如图14-6所示。矩形的上边缘代表模块的功能,或其公共接口的复杂性。更宽的矩形代表更广泛的功能,而更窄的矩形具有更受限制的功能,因此公共接口更简单。矩形的区域代表模块的逻辑,或者它的功能的实现。
Ousterhout proposes to visualize a module as a rectangle, as shown in Figure 14-6. The rectangle’s top edge represents the module’s function, or the complexity of its public interface. A wider rectangle represents broader functionality, while a narrower one has a more restricted function and thus a simpler public interface. The area of the rectangle represents the module’s logic, or the implementation of its functionality.
根据这个模型,有效的模块很深:一个简单的公共接口封装了复杂的逻辑。无效模块是浅层的:浅层模块的公共接口比深层模块封装的复杂性要低得多。考虑以下清单中的方法:
According to this model, effective modules are deep: a simple public interface encapsulates complex logic. Ineffective modules are shallow: a shallow module’s public interface encapsulates much less complexity than a deep module. Consider the method in the following listing:
intAddTwoNumbers(inta,intb){returna+b;}
intAddTwoNumbers(inta,intb){returna+b;}
这是浅模块的极端情况:公共接口(方法的签名)及其逻辑(方法)完全相同。拥有这样一个模块会引入无关的“移动部件”,因此,它不会封装复杂性,而是给总体系统增加了意外的复杂性。
This is the extreme case of a shallow module: the public interface (the method’s signature) and its logic (the methods) are exactly the same. Having such a module introduces extraneous “moving parts,” and thus, instead of encapsulating complexity, it adds accidental complexity to the overarching system.
除了不同的术语,深度模块的概念也不同来自微服务模式,因为模块可以表示逻辑和物理边界,而微服务是严格的物理边界。否则,这两个概念及其基本设计原则是相同的。
Apart from different terminology, the notion of deep modules differs from the microservices pattern in that the modules can denote both logical and physical boundaries, while microservices are strictly physical. Otherwise, both concepts and their underlying design principles are the same.
如图 14-3所示,实现单个业务方法的服务是浅模块。因为我们必须引入与集成相关的公共方法,所以生成的接口比应有的“更宽”。
The services implementing a single business method, shown in Figure 14-3, are shallow modules. Because we had to introduce integration-related public methods, the resultant interfaces are “wider” than they should have been.
从系统复杂性的角度来看,深层模块降低了系统的全局复杂性,而浅层模块通过引入不封装其局部复杂性的组件来增加系统的复杂性。
From a system complexity standpoint, a deep module reduces the system’s global complexity, while a shallow module increases it by introducing a component that doesn’t encapsulate its local complexity.
浅层服务也是许多面向微服务的项目失败的原因。将微服务错误地定义为不超过X行代码的服务,或者应该更容易重写而不是修改的服务,将注意力集中在单个服务上,而忽略了架构最重要的方面:系统。
Shallow services are also the reason why so many microservices-oriented projects fail. The mistaken definitions of a microservice as a service having no more than X lines of code, or as a service that should be easier to rewrite than to modify, concentrate on the individual service while missing the most important aspect of the architecture: the system.
系统可以分解为微服务的阈值由微服务所属的系统用例定义。如果我们将整体分解为服务,引入变更的成本就会下降。当系统被分解成微服务时,它被最小化。但是,如果你继续分解超过微服务的门槛,深层服务将变得越来越浅。他们的接口将重新增长。这一次,由于集成的需要,引入一个变更的成本也将上升,整个系统的架构将变成可怕的分布式大泥球。如图 14-7所示。
The threshold upon which a system can be decomposed into microservices is defined by the use cases of the system that the microservices are a part of. If we decompose a monolith into services, the cost of introducing a change goes down. It is minimized when the system is decomposed into microservices. However, if you keep decomposing past the microservices threshold, the deep services will become more and more shallow. Their interfaces will grow back up. This time, due to integration needs, the cost of introducing a change will go up as well, and the overall system’s architecture will turn into the dreaded distributed big ball of mud. This is depicted in Figure 14-7.
了解了微服务是什么之后,我们来看看领域驱动设计如何帮助我们找到深度服务的边界。
Now that we’ve learned what microservices are, let’s take a look at how domain-driven design can help us find the boundaries of deep services.
作为微服务,许多领域驱动的设计模式前几章讨论的是边界:有界上下文是模型的边界,子域是业务能力的边界,而聚合和值对象是事务边界。让我们看看这些边界中的哪些适用于微服务的概念。
As microservices, many of the domain-driven design patterns discussed in the previous chapters are about boundaries: the bounded context is the boundary of a model, a subdomain bounds a business capability, while aggregate and value objects are transactional boundaries. Let’s see which of these boundaries lends itself to the notion of microservices.
微服务和限界上下文模式有很多共同点,如此之多,以至于这些模式经常可以互换使用。让我们看看情况是否真的如此:有界上下文的边界是否与有效微服务的边界相关?
The microservices and bounded context patterns have a lot in common, so much so that the patterns are often used interchangeably. Let’s see whether that’s really the case: do bounded contexts’ boundaries correlate with the boundaries of effective microservices?
微服务和限界上下文都是物理边界。微服务作为限界上下文,由单个团队拥有。与限界上下文一样,相互冲突的模型无法在微服务中实现,从而导致接口复杂。微服务确实是有界上下文。但这种关系是否反过来呢?我们可以说限界上下文是微服务吗?
Both microservices and bounded contexts are physical boundaries. Microservices, as bounded contexts, are owned by a single team. As in bounded contexts, conflicting models cannot be implemented in a microservice, resulting in complex interfaces. Microservices are indeed bounded contexts. But does this relationship work the other way around? Can we say that bounded contexts are microservices?
正如您在第 3 章中了解到的,限界上下文保护无处不在的语言和模型的一致性。在相同的限界上下文中不能实现相互冲突的模型。假设您正在开发广告管理系统。在系统的业务领域中,业务实体 Lead 在 Promotions 和 Sales 上下文中由不同的模型表示。因此,Promotions 和 Sales 是有界上下文,每个都定义了一个并且只有一个 Campaign 实体模型,该模型在其边界内有效,如图14-8所示。
As you learned in Chapter 3, bounded contexts protect the consistency of ubiquitous languages and models. No conflicting models can be implemented in the same bounded context. Say you are working on an advertising management system. In the system’s business domain, the business entity Lead is represented by different models in the Promotions and Sales contexts. Hence, Promotions and Sales are bounded contexts, each defining one and only one model of the Campaign entity, which is valid in its boundary, as shown in Figure 14-8.
为简单起见,我们假设系统中除了 Lead 之外没有其他冲突模型。这使得生成的限界上下文自然很宽——每个限界上下文可以包含多个子域。子域可以从一个限界上下文移动到另一个限界上下文。只要子域不暗示冲突模型,图 14-9中的所有替代分解都是完全有效的限界上下文。
For simplicity’s sake, let’s assume there are no other conflicting models in the system besides Lead. This makes the resultant bounded contexts naturally wide—each bounded context can contain multiple subdomains. The subdomains can be moved from one bounded context to another one. As long as the subdomains do not imply conflicting models, all the alternative decompositions in Figure 14-9 are perfectly valid bounded contexts.
对限界上下文的不同分解归因于不同的需求,例如不同的团队规模和结构、生命周期依赖性等。但是我们能说这个例子中所有有效的限界上下文都必然是微服务吗?不。特别是考虑到分解 1 中两个限界上下文的相对广泛的功能。
The different decompositions to bounded contexts attribute different requirements, such as different teams’ sizes and structures, lifecycle dependencies, and so on. But can we say that all the valid bounded contexts in this example are necessarily microservices? No. Especially considering the relatively wide functionalities of the two bounded contexts in decomposition 1.
因此,微服务和限界上下文之间的关系不是对称的。尽管微服务是有界上下文,但并非每个有界上下文都是微服务。另一方面,有界上下文表示最大的有效单体的边界。不应将这样的巨石与一大团泥混为一谈;这是一个可行的设计选项,可以保护其通用语言或其业务领域模型的一致性。正如我们将在第 15 章中讨论的那样,在某些情况下,如此宽泛的边界比微服务更有效。
Therefore, the relationship between microservices and bounded contexts is not symmetric. Although microservices are bounded contexts, not every bounded context is a microservice. Bounded contexts, on the other hand, denote the boundaries of the largest valid monolith. Such a monolith should not be confused with a big ball of mud; it’s a viable design option that protects the consistency of its ubiquitous language, or its model of the business domain. As we will discuss in Chapter 15, such broad boundaries are more effective than microservices in certain cases.
图 14-10直观地展示了限界上下文和微服务之间的关系。限界上下文和微服务之间的区域是安全的。这些是有效的设计选项。但是,如果系统没有分解到适当的限界上下文或分解超过微服务阈值,则会分别导致大泥球或分布式大泥球。
Figure 14-10 visually demonstrates the relationship between bounded contexts and microservices. The area between the bounded contexts and microservices is safe. These are valid design options. However, if the system is not decomposed into proper bounded contexts or is decomposed past the microservices threshold, it will result in a big ball of mud or a distributed big ball of mud, respectively.
接下来,让我们检查另一个极端:聚合是否可以帮助找到微服务的边界。
Next, let’s examine the other extreme: whether aggregates can help find the microservices’ boundaries.
虽然有界上下文对最宽的有效边界施加了限制,聚合模式则相反。聚合的边界是可能的最窄边界。将聚合分解为多个物理服务或限界上下文不仅不是最佳选择,而且正如您将在附录 A中了解到的那样,至少可以说会导致不良后果。
While bounded contexts impose limits on the widest valid boundaries, the aggregate pattern does the opposite. The aggregate’s boundary is the narrowest boundary possible. Decomposing an aggregate into multiple physical services, or bounded contexts, is not only suboptimal but, as you will learn in Appendix A, leads to undesired consequences, to say the least.
作为有界上下文,聚合的边界也经常被认为是驱动微服务的边界。聚合是一个不可分割的业务功能单元,它封装了其内部业务规则、不变量和逻辑的复杂性。也就是说,正如您在本章前面了解到的那样,微服务与单个服务无关。必须在与系统其他组件交互的上下文中考虑单个服务:
As bounded contexts, aggregates’ boundaries are also often considered to drive the boundaries of microservices. An aggregate is an indivisible business functionality unit that encapsulates the complexities of its internal business rules, invariants, and logic. That said, as you learned earlier in this chapter, microservices are not about individual services. An individual service has to be considered in the context of its interactions with other components of the system:
有问题的聚合是否与其子域中的其他聚合通信?
Does the aggregate in question communicate with other aggregates in its subdomain?
它是否与其他聚合共享值对象?
Does it share value objects with other aggregates?
聚合的业务逻辑更改影响子域的其他组件的可能性有多大,反之亦然?
How likely will the aggregate’s business logic changes affect other components of the subdomain and vice versa?
聚合与其子域的其他业务实体的关系越强,作为单个服务的关系就越浅。
The stronger the aggregate’s relationship is with the other business entities of its subdomain, the shallower it will be as an individual service.
在某些情况下,将聚合作为服务会产生模块化设计。然而,更常见的是,这种细粒度的服务会增加总体系统的全局复杂性。
There will be cases in which having an aggregate as a service will produce a modular design. However, much more often such fine-grained services will increase the overarching system’s global complexity.
设计微服务的更平衡的启发式是使服务与业务子域的边界保持一致。正如您在第 1 章中了解到的,子域与细粒度的业务功能相关联。这些是公司在其业务领域竞争所需的业务构建块。从业务领域的角度来看,子域描述了能力——业务做什么——而不解释能力是如何实现的。从技术角度来看,子域代表一组连贯的用例:使用相同的业务域模型,处理相同或密切相关的数据,并具有强大的功能关系。如图 14-11所示,其中一个用例的业务需求变化很可能会影响其他用例。
A more balanced heuristic for designing microservices is to align the services with the boundaries of business subdomains. As you learned in Chapter 1, subdomains are correlated with fine-grained business capabilities. These are the business building blocks required for the company to compete in its business domain(s). From a business domain perspective, subdomains describe the capabilities—what the business does—without explaining how the capabilities are implemented. From a technical standpoint, subdomains represent sets of coherent use cases: using the same model of the business domain, working on the same or closely related data, and having a strong functional relationship. A change in the business requirements of one of the use cases is likely to affect the other use cases, as shown in Figure 14-11.
子域的粒度和对功能的关注——“什么”而不是“如何”——使得子域自然是深层模块。子域的描述(功能)封装了更复杂的实现细节(逻辑)。子域中包含的用例的连贯性也确保了最终模块的深度。在许多情况下将它们分开会导致更复杂的公共接口和更浅的模块。所有这些都使子域成为设计微服务的安全边界。
The subdomains’ granularity and the focus on the functionality—the “what” rather than the “how”—makes subdomains naturally deep modules. A subdomain’s description—the function—encapsulates the more complex implementation details—the logic. The coherent nature of the use cases contained in a subdomain also ensures the resultant module’s depth. Splitting them apart in many cases would result in a more complex public interface and thus shallower modules. All of these things make subdomains a safe boundary for designing microservices.
将微服务与子域对齐是一种安全的启发式方法,可以为大多数微服务生成最佳解决方案。也就是说,在某些情况下,其他边界会更有效;例如,停留在有界上下文的更广泛的语言边界内,或者由于非功能性需求而求助于聚合作为微服务。解决方案不仅取决于业务领域,还取决于组织的结构、业务战略和非功能性需求。正如我们在第 11 章中讨论的那样,不断调整软件架构和设计以适应环境的变化至关重要。
Aligning microservices with subdomains is a safe heuristic that produces optimal solutions for the majority of microservices. That said, there will be cases where other boundaries will be more efficient; for example, staying in the wider, linguistic boundaries of the bounded context or, due to nonfunctional requirements, resorting to an aggregate as a microservice. The solution depends not only on the business domain but also on the organization’s structure, business strategy, and nonfunctional requirements. As we discussed in Chapter 11, it’s crucial to continuously adapt the software architecture and design to changes in the environment.
除了寻找服务边界之外,领域驱动设计还可以帮助使服务更深入。本节演示开放主机服务和反腐败层模式如何简化微服务的公共接口。
In addition to finding service boundaries, domain-driven design can help make services deeper. This section demonstrates how the open-host service and anticorruption layer patterns can simplify the microservices’ public interfaces.
开放主机服务解耦了有界上下文的模型从模型中提取业务域,用于与系统的其他组件集成,如图14-12所示。
The open-host service decouples the bounded context’s model of the business domain from the model used for integration with other components of the system, as shown in Figure 14-12.
引入面向集成的模型,已发布的语言,降低了系统的全局复杂性。首先,它允许我们在不影响其消费者的情况下改进服务的实现:新的实现模型可以转换为现有的已发布语言。其次,已发表的语言暴露了一种更加克制的模式。它是围绕集成需求设计的。它封装了与服务消费者无关的实现的复杂性。例如,它可以公开更少的数据,并为消费者提供更方便的模型。
Introducing the integration-oriented model, the published language, reduces the system’s global complexity. First, it allows us to evolve the service’s implementation without impacting its consumers: the new implementation model can be translated to the existing published language. Second, the published language exposes a more restrained model. It is designed around integration needs. It encapsulates the complexity of the implementation that is not relevant to the service’s consumers. For example, it can expose less data and in a more convenient model for consumers.
在相同的实现(逻辑)上使用更简单的公共接口(函数)可以使服务“更深入”并有助于更有效的微服务设计。
Having a simpler public interface (function) over the same implementation (logic) makes the service “deeper” and contributes to a more effective microservice design.
反腐败层 (ACL) 模式以相反的方式工作。它降低了将服务与其他有界上下文集成的复杂性。传统上,反腐败层属于它所保护的限界上下文。然而,正如我们在第 9 章中讨论的那样,这个概念可以更进一步并作为一个独立的服务来实现。
The anticorruption layer (ACL) pattern works the other way around. It reduces the complexity of integrating the service with other bounded contexts. Traditionally, the anticorruption layer belongs to the bounded context it protects. However, as we discussed in Chapter 9, this notion can be taken a step further and implemented as a standalone service.
图 14-13中的 ACL 服务降低了使用边界上下文的局部复杂性和系统的全局复杂性。消费限界上下文的业务复杂性与集成复杂性分离。后者被卸载到 ACL 服务。因为消费限界上下文使用更方便的、面向集成的模型,所以它的公共接口被压缩——它不反映生产服务暴露的集成复杂性。
The ACL service in Figure 14-13 reduces both the local complexity of the consuming bounded context and the system’s global complexity. The consuming bounded context’s business complexity is separated from the integration complexity. The latter is offloaded to the ACL service. Because the consuming bounded context is working with a more convenient, integration-oriented model, its public interface is compressed—it doesn’t reflect the integration complexity exposed by the producing service.
从历史上看,基于微服务的架构风格与领域驱动设计密切相关,以至于术语微服务和限界上下文经常互换使用。本章我们分析了两者的联系,发现它们不是一回事。
Historically, the microservice-based architectural style is deeply interconnected with domain-driven design, so much so that the terms microservice and bounded context are often used interchangeably. In this chapter, we analyzed the connection between the two and saw that they are not the same thing.
所有微服务都是限界上下文,但并非所有限界上下文都一定是微服务。从本质上讲,微服务定义了服务的最小有效边界,而有界上下文保护所包含模型的一致性并代表最宽的有效边界。将边界定义为比其有界上下文更宽会导致一团泥泞,而比微服务更小的边界将导致一团分布式的大泥团。
All microservices are bounded contexts, but not all bounded contexts are necessarily microservices. In its essence, a microservice defines the smallest valid boundary of a service, while a bounded context protects the consistency of the encompassed model and represents the widest valid boundaries. Defining boundaries to be wider than their bounded contexts will result in a big ball of mud, while boundaries that are smaller than microservices will lead to a distributed big ball of mud.
然而,微服务和领域驱动设计之间的联系是紧密的。我们看到了如何使用领域驱动设计工具来设计有效的微服务边界。
Nevertheless, the connection between microservices and domain-driven design is tight. We saw how domain-driven design tools can be used to design effective microservice boundaries.
在第 15 章中,我们将从不同的角度继续讨论高级系统架构:通过事件驱动架构进行异步集成。您将学习如何利用不同类型的事件消息来进一步优化微服务的边界。
In Chapter 15, we will continue discussing high-level system architecture but from a different perspective: asynchronous integration through event-driven architecture. You will learn how to leverage the different kinds of event messages to further optimize microservices’ boundaries.
限界上下文和微服务之间的关系是什么?
所有微服务都是有界上下文。
所有限界上下文都是微服务。
微服务和限界上下文是同一概念的不同术语。
微服务和限界上下文是完全不同的概念,不能相提并论。
What is the relationship between bounded contexts and microservices?
All microservices are bounded contexts.
All bounded contexts are microservices.
Microservices and bounded contexts are different terms for the same concept.
Microservices and bounded contexts are completely different concepts and cannot be compared.
微服务的哪一部分应该是“微”的?
养活实施微服务的团队所需的披萨数量。该指标必须考虑团队成员不同的饮食偏好和平均每日卡路里摄入量。
实现服务功能所需的代码行数。由于该指标与线条的宽度无关,因此最好在超宽显示器上实施微服务。
设计基于微服务的系统最重要的方面是获得微服务友好的中间件和其他基础组件,最好来自微服务认证的供应商。
业务领域的知识及其跨服务边界公开并由其公共接口反映的复杂性。
What part of a microservice should be “micro”?
The number of pizzas required to feed the team implementing the microservices. The metric has to take into account the team members’ different dietary preferences and average daily calorie intakes.
The number of lines of code it takes to implement the service’s functionality. Since the metric is agnostic of the lines’ widths, it’s preferable to implement microservices on ultrawide monitors.
The most important aspect of designing microservices-based systems is to get microservices-friendly middleware and other infrastructural components, preferably from microservices-certified vendors.
The knowledge of the business domain and its intricacies exposed across the service’s boundary and reflected by its public interface.
什么是安全组件边界?
边界比有界上下文更宽。
边界比微服务更窄。
有界上下文(最宽)和微服务(最窄)之间的边界。
所有边界都是安全的。
What are the safe component boundaries?
Boundaries wider than bounded contexts.
Boundaries narrower than microservices.
Boundaries between bounded contexts (widest) and microservices (narrowest).
All boundaries are safe.
将微服务与聚合的边界对齐是一个好的设计决策吗?
是的,聚合总能提供适当的微服务。
不,聚合永远不应该作为单独的微服务公开。
不可能从单个聚合中创建微服务。
该决定取决于业务领域。
Is it a good design decision to align microservices with the boundaries of aggregates?
Yes, aggregates always make for proper microservices.
No, aggregates should never be exposed as individual microservices.
It’s impossible to make a microservice out of a single aggregate.
The decision depends on the business domain.
作为微服务,事件驱动架构 (EDA) 无处不在在现代分布式系统中。许多人建议在设计松散耦合、可扩展、容错的分布式系统时使用事件驱动通信作为默认集成机制。
As microservices, event-driven architecture (EDA) is ubiquitous in modern distributed systems. Many advise using event-driven communication as the default integration mechanism when designing loosely coupled, scalable, fault-tolerant distributed systems.
事件驱动架构通常与领域驱动设计相关联。毕竟,EDA 是基于事件的,而事件在 DDD 中是突出的——我们有领域事件,在需要的时候,我们甚至将事件作为系统的真实来源。利用 DDD 的事件作为使用事件驱动架构的基础可能很诱人。但这是个好主意吗?
Event-driven architecture is often linked to domain-driven design. After all, EDA is based on events, and events are prominent in DDD—we have domain events, and when needed, we even use events as the system’s source of truth. It may be tempting to leverage DDD’s events as the basis for using event-driven architecture. But is this a good idea?
事件不是一种秘方,您可以将其倒在遗留系统上并将其变成松散耦合的分布式系统。恰恰相反:EDA 的粗心应用可能会将模块化单体变成分布式的大泥球。
Events are not a kind of secret sauce that you can just pour over a legacy system and turn it into a loosely coupled distributed system. Quite the opposite: careless application of EDA can turn a modular monolith into a distributed big ball of mud.
在本章中,我们将探讨 EDA 和 DDD 之间的相互作用。您将了解事件驱动架构的基本构建块、EDA 项目失败的常见原因,以及如何利用 DDD 的工具来设计有效的异步集成系统。
In this chapter, we will explore the interplay between EDA and DDD. You will learn the essential building blocks of event-driven architecture, common causes for failed EDA projects, and how you can leverage DDD’s tools to design effective, asynchronously integrated systems.
简单地说,事件驱动架构是一种架构风格其中系统的组件通过交换事件消息相互异步通信(见图15-1)。组件不是同步调用服务的端点,而是发布事件以通知其他系统元素系统域中的更改。这些组件可以订阅系统中引发的事件并做出相应的反应。事件驱动执行流的典型示例是第 9 章中描述的传奇模式。
Stated simply, event-driven architecture is an architectural style in which a system’s components communicate with one another asynchronously by exchanging event messages (see Figure 15-1). Instead of calling the services’ endpoints synchronously, the components publish events to notify other system elements of changes in the system’s domain. The components can subscribe to events raised in the system and react accordingly. A typical example of an event-driven execution flow is the saga pattern that was described in Chapter 9.
强调两者之间的区别很重要事件驱动架构和事件溯源。正如我们在第 7 章中讨论的那样,事件溯源是一种将状态变化捕获为一系列事件的方法。
It’s important to highlight the difference between event-driven architecture and event sourcing. As we discussed in Chapter 7, event sourcing is a method for capturing changes in state as a series of events.
尽管事件驱动架构和事件溯源都是基于事件的,但这两种模式在概念上是不同的。EDA 指的是服务之间的通信,而事件溯源发生在服务内部。为事件溯源设计的事件表示在服务中实现的状态转换(事件溯源域模型中的聚合)。它们旨在捕获业务领域的复杂性,而不是为了将服务与其他系统组件集成。
Although both event-driven architecture and event sourcing are based on events, the two patterns are conceptually different. EDA refers to the communication between services, while event sourcing happens inside a service. The events designed for event sourcing represent state transitions (of aggregates in an event-sourced domain model) implemented in the service. They are aimed at capturing the intricacies of the business domain and are not intended to integrate the service with other system components.
正如您将在本章后面看到的那样,共有三种类型的事件,其中一些比其他的更适合集成。
As you will see later in this chapter, there are three types of events, and some are more suited for integration than others.
在 EDA 系统中,事件的交换是关键的通信用于集成组件并使它们成为一个系统的机制。让我们更详细地了解一下事件,看看它们与消息有何不同。
In an EDA system, the exchange of events is the key communication mechanism for integrating the components and making them a system. Let’s take a look at events in more detail and see how they differ from messages.
到目前为止,事件的定义类似于消息模式的定义。1然而,两者是不同的。事件是消息,但消息不一定是事件。有两种类型的消息:
So far, the definition of an event is similar to the definition of the message pattern.1 However, the two are different. An event is a message, but a message is not necessarily an event. There are two types of messages:
事件是已经发生的事情,而命令是做某事的指令。事件和命令都可以作为消息异步传送。但是,可以拒绝命令:命令的目标可以拒绝执行命令,例如,如果命令无效或与系统的业务规则相矛盾。另一方面,事件的接收者不能取消该事件。事件描述已经发生的事情。要推翻事件,唯一可以做的就是发出一个补偿动作——一个命令,就像在 saga 模式中执行的那样。
An event is something that has already happened, whereas a command is an instruction to do something. Both events and commands can be communicated asynchronously as messages. However, a command can be rejected: the command’s target can refuse to execute the command, for example, if the command is invalid or if it contradicts the system’s business rules. A recipient of an event, on the other hand, cannot cancel the event. The event describes something that has already happened. The only thing that can be done to overturn an event is to issue a compensating action—a command, as it’s carried out in the saga pattern.
由于事件描述的是已经发生的事情,事件的名称应使用过去时:例如,DeliveryScheduled、ShipmentCompleted 或 DeliveryConfirmed。
Since an event describes something that has already happened, an event’s name should be formulated in the past tense: for example, DeliveryScheduled, ShipmentCompleted, or DeliveryConfirmed.
事件是可以序列化和传输的数据记录使用所选的消息传递平台。典型的事件模式包括事件的元数据及其有效负载——事件传达的信息:
An event is a data record that can be serialized and transmitted using the messaging platform of choice. A typical event schema includes the event’s metadata and its payload—the information communicated by the event:
{"type":"delivery-confirmed","event-id":"14101928-4d79-4da6-9486-dbc4837bc612","correlation-id":"08011958-6066-4815-8dbe-dee6d9e5ebac","delivery-id":"05011927-a328-4860-a106-737b2929db4e","timestamp":1615718833,"payload":{"confirmed-by":"17bc9223-bdd6-4382-954d-f1410fd286bd","delivery-time":1615701406}}
{"type":"delivery-confirmed","event-id":"14101928-4d79-4da6-9486-dbc4837bc612","correlation-id":"08011958-6066-4815-8dbe-dee6d9e5ebac","delivery-id":"05011927-a328-4860-a106-737b2929db4e","timestamp":1615718833,"payload":{"confirmed-by":"17bc9223-bdd6-4382-954d-f1410fd286bd","delivery-time":1615701406}}
事件的有效负载不仅描述了事件传递的信息,还定义了事件的类型。让我们详细讨论这三种类型的事件以及它们之间的区别。
An event’s payload not only describes the information conveyed by the event, but also defines the event’s type. Let’s discuss the three types of events in detail and how they differ from one another.
事件可以分为以下三种类型之一:2事件通知、事件承载状态转移或领域事件。
Events can be categorized into one of three types:2 event notification, event-carried state transfer, or domain events.
事件通知是有关业务领域更改的消息其他组件会做出反应。示例包括 PaycheckGenerated 和 CampaignPublished 等。
An event notification is a message regarding a change in the business domain that other components will react to. Examples include PaycheckGenerated and CampaignPublished, among others.
事件通知不应该冗长:目标是通知感兴趣的各方有关事件,但通知不应包含订阅者对事件做出反应所需的所有信息。例如:
The event notification should not be verbose: the goal is to notify the interested parties about the event, but the notification shouldn’t carry all the information needed for the subscribers to react to the event. For example:
{"type":"paycheck-generated","event-id":"537ec7c2-d1a1-2005-8654-96aee1116b72","delivery-id":"05011927-a328-4860-a106-737b2929db4e","timestamp":1615726445,"payload":{"employee-id":"456123","link":"/paychecks/456123/2021/01"}}
{"type":"paycheck-generated","event-id":"537ec7c2-d1a1-2005-8654-96aee1116b72","delivery-id":"05011927-a328-4860-a106-737b2929db4e","timestamp":1615726445,"payload":{"employee-id":"456123","link":"/paychecks/456123/2021/01"}}
在前面的代码中,事件通知外部组件已生成薪水。它不包含与薪水相关的所有信息。相反,接收者可以使用链接获取更详细的信息。这个通知流程如图 15-2所示。
In the preceding code, the event notifies the external components of a paycheck that was generated. It doesn’t carry all the information related to the paycheck. Instead, the receiver can use the link to fetch more detailed information. This notification flow is depicted in Figure 15-2.
从某种意义上说,通过事件通知消息进行集成类似于美国的无线紧急警报(WEA)系统和欧洲的EU-Alert(见图15-3)。该系统使用手机信号塔广播短消息,通知市民有关公共卫生问题、安全威胁和其他紧急情况。系统仅限于发送最大长度为 360 个字符的消息。这条短消息足以通知您紧急情况,但您必须主动使用其他信息源来获取更多详细信息。
In a sense, integration through event notification messages is similar to the Wireless Emergency Alert (WEA) system in the United States and EU-Alert in Europe (see Figure 15-3). The systems use cell towers to broadcast short messages, notifying citizens about public health concerns, safety threats, and other emergencies. The systems are limited to sending messages with a maximum length of 360 characters. This short message is enough to notify you about the emergency, but you have to proactively use other information sources to get more details.
在多种情况下,简洁的事件通知可能更可取。让我们仔细看看两个:安全性和并发性。
Succinct event notifications can be preferable in multiple scenarios. Let’s take a closer look at two: security and concurrency.
由于事件驱动集成的异步特性,信息到达订户时可能已经过时。如果信息的性质对竞争条件敏感,则明确查询它可以获取最新状态。
Due to the asynchronous nature of event-driven integration, the information can already be rendered stale when it reaches the subscribers. If the information’s nature is sensitive to race conditions, querying it explicitly allows getting the up-to-date state.
此外,在并发消费者的情况下,只有一个订阅者应该处理一个事件,查询过程可以与悲观锁定集成。这确保生产者一方没有其他消费者能够处理该消息。
Furthermore, in the case of concurrent consumers, where only one subscriber should process an event, the querying process can be integrated with pessimistic locking. This ensures the producer’s side that no other consumer will be able to process the message.
事件承载状态传输 (ECST) 消息通知订阅者关于生产者内部状态的变化。与事件通知消息相反,ECST 消息包含反映状态变化的所有数据。
Event-carried state transfer (ECST) messages notify subscribers about changes in the producer’s internal state. Contrary to event notification messages, ECST messages include all the data reflecting the change in the state.
ECST 消息可以有两种形式。第一个是修改实体状态的完整快照:
ECST messages can come in two forms. The first is a complete snapshot of the modified entity’s state:
{"type":"customer-updated","event-id":"6b7ce6c6-8587-4e4f-924a-cec028000ce6","customer-id":"01b18d56-b79a-4873-ac99-3d9f767dbe61","timestamp":1615728520,"payload":{"first-name":"Carolyn","last-name":"Hayes","phone":"555-1022","status":"follow-up-set","follow-up-date":"2021/05/08","birthday":"1982/04/05","version":7}}
{"type":"customer-updated","event-id":"6b7ce6c6-8587-4e4f-924a-cec028000ce6","customer-id":"01b18d56-b79a-4873-ac99-3d9f767dbe61","timestamp":1615728520,"payload":{"first-name":"Carolyn","last-name":"Hayes","phone":"555-1022","status":"follow-up-set","follow-up-date":"2021/05/08","birthday":"1982/04/05","version":7}}
前面示例中的 ECST 消息包括客户更新状态的完整快照。操作大型数据结构时,只在 ECST 消息中包含实际修改的字段可能是合理的:
The ECST message in the preceding example includes a complete snapshot of a customer’s updated state. When operating large data structures, it may be reasonable to include in the ECST message only the fields that were actually modified:
{"type":"customer-updated","event-id":"6b7ce6c6-8587-4e4f-924a-cec028000ce6","customer-id":"01b18d56-b79a-4873-ac99-3d9f767dbe61","timestamp":1615728520,"payload":{"status":"follow-up-set","follow-up-date":"2021/05/10","version":8}}
{"type":"customer-updated","event-id":"6b7ce6c6-8587-4e4f-924a-cec028000ce6","customer-id":"01b18d56-b79a-4873-ac99-3d9f767dbe61","timestamp":1615728520,"payload":{"status":"follow-up-set","follow-up-date":"2021/05/10","version":8}}
无论 ECST 消息包含完整的快照还是仅包含更新的字段,此类事件流都允许消费者保存实体状态的本地缓存并使用它。从概念上讲,使用事件携带的状态传输消息是一种异步数据复制机制。这种方法使系统更具容错性,这意味着即使生产者不可用,消费者也可以继续运行。这也是提高必须处理来自多个来源的数据的组件性能的一种方法。无需每次需要数据时都去查询数据源,可以将所有数据缓存在本地,如图15-4所示。
Whether ECST messages include complete snapshots or only the updated fields, a stream of such events allows consumers to hold a local cache of the entities’ states and work with it. Conceptually, using event-carried state transfer messages is an asynchronous data replication mechanism. This approach makes the system more fault tolerant, meaning that the consumers can continue functioning even if the producer is not available. It is also a way to improve the performance of components that have to process data from multiple sources. Instead of querying the data sources each time the data is needed, all the data can be cached locally, as shown in Figure 15-4.
第三类事件消息是我们描述的领域事件在第 6 章中。在某种程度上,领域事件介于事件通知和 ECST 消息之间:它们都描述了业务领域中的重要事件,并且包含描述该事件的所有数据。尽管有相似之处,但这些类型的消息在概念上是不同的。
The third type of event message is the domain event that we described in Chapter 6. In a way, domain events are somewhere between event notification and ECST messages: they both describe a significant event in the business domain, and they contain all the data describing the event. Despite the similarities, these types of messages are conceptually different.
域事件和事件通知都描述了生产商业务领域的变化。也就是说,有两个概念上的差异。
Both domain events and event notifications describe changes in the producer’s business domain. That said, there are two conceptual differences.
首先,领域事件包括描述事件的所有信息。消费者不需要采取任何进一步的行动来获得完整的画面。
First, domain events include all the information describing the event. The consumer does not need to take any further action to get the complete picture.
二是造型意图不同。事件通知旨在减轻与其他组件的集成。另一方面,领域事件旨在建模和描述业务领域。即使没有外部消费者对它们感兴趣,领域事件也是有用的。在事件源系统中尤其如此,在该系统中领域事件用于模拟所有可能的状态转换。让外部消费者对所有可用的领域事件感兴趣会导致设计欠佳。我们将在本章后面更详细地讨论这个问题。
Second, the modeling intent is different. Event notifications are designed with the intent to alleviate integration with other components. Domain events, on the other hand, are intended to model and describe the business domain. Domain events can be useful even if no external consumer is interested in them. That’s especially true in event-sourced systems, where domain events are used to model all possible state transitions. Having external consumers interested in all the available domain events would result in suboptimal design. We will discuss this in greater detail later in this chapter.
领域事件中包含的数据在概念上是不同的来自典型 ECST 消息的模式。
The data contained in domain events is conceptually different from the schema of a typical ECST message.
ECST 消息提供了足够的信息来保存生产者数据的本地缓存。任何单一领域事件都不应该公开如此丰富的模型。即使包含在特定领域事件中的数据也不足以缓存聚合的状态,因为消费者未订阅的其他领域事件可能会影响相同的字段。
An ECST message provides sufficient information to hold a local cache of the producer’s data. No single domain event is supposed to expose such a rich model. Even the data included in a specific domain event is not sufficient for caching the aggregate’s state, as other domain events that the consumer is not subscribed to may affect the same fields.
此外,与通知事件的情况一样,两种消息的建模意图也不同。域事件中包含的数据无意描述聚合的状态。相反,它描述了在其生命周期内发生的业务事件。
Furthermore, as in the case of notification events, the modeling intent is different for the two types of messages. The data included in domain events is not intended to describe the aggregate’s state. Instead, it describes a business event that happened during its lifecycle.
这是一个演示之间差异的示例事件的三种类型。考虑以下三种表示婚姻事件的方法:
Here is an example that demonstrates the differences between the three types of events. Consider the following three ways to represent the event of marriage:
eventNotification={"type":"marriage-recorded","person-id":"01b9a761","payload":{"person-id":"126a7b61","details":"/01b9a761/marriage-data"}};ecst={"type":"personal-details-changed","person-id":"01b9a761","payload":{"new-last-name":"Williams"}};domainEvent={"type":"married","person-id":"01b9a761","payload":{"person-id":"126a7b61","assumed-partner-last-name":true}};
eventNotification={"type":"marriage-recorded","person-id":"01b9a761","payload":{"person-id":"126a7b61","details":"/01b9a761/marriage-data"}};ecst={"type":"personal-details-changed","person-id":"01b9a761","payload":{"new-last-name":"Williams"}};domainEvent={"type":"married","person-id":"01b9a761","payload":{"person-id":"126a7b61","assumed-partner-last-name":true}};
marriage-recorded是事件通知消息。除了具有指定 ID 的人结婚这一事实外,它不包含任何信息。它包含有关事件的最少信息,对更多详细信息感兴趣的消费者将不得不点击该details字段中的链接。
marriage-recorded is an event notification message. It contains no information except the fact that the person with the specified ID got married. It contains minimal information about the event, and the consumers interested in more details will have to follow the link in the details field.
personal-details-changed是事件携带的状态转移消息。它描述了此人的个人详细信息的变化,即他们的姓氏已更改。该消息没有解释它发生变化的原因。这个人结婚或离婚了吗?
personal-details-changed is an event-carried state transfer message. It describes the changes in the person’s personal details, namely that their last name has been changed. The message doesn’t explain the reason why it has changed. Did the person get married or divorced?
最后,married是一个领域事件。它的建模尽可能接近业务领域中事件的性质。它包括此人的 ID 和一个标志,指示此人是否使用了其伴侣的名字。
Finally, married is a domain event. It is modeled as close as possible to the nature of the event in the business domain. It includes the person’s ID and a flag indicating whether the person assumed their partner’s name.
正如我们在第 3 章中讨论的那样,软件设计主要是关于界限。边界定义了什么属于内部,什么在外部,最重要的是,什么跨越了边界——本质上,组件如何相互集成。基于 EDA 的系统中的事件是一流的设计元素,影响组件的集成方式和组件本身的边界。选择正确的事件消息类型是使(解耦)或破坏(耦合)分布式系统的原因。
As we discussed in Chapter 3, software design is predominantly about boundaries. Boundaries define what belongs inside, what remains outside, and most importantly, what goes across the boundaries—essentially, how the components are integrated with one another. The events in an EDA-based system are first-class design elements, affecting both how the components are integrated and the components’ boundaries themselves. Choosing the correct type of event message is what makes (decouples) or breaks (couples) a distributed system.
在本节中,您将学习应用不同事件类型的启发式方法。但首先,让我们看看如何使用事件来设计一个强耦合的分布式大泥球。
In this section, you will learn heuristics for applying different event types. But first, let’s see how to use events to design a strongly coupled, distributed big ball of mud.
考虑图 15-5中所示的系统。
Consider the system shown in Figure 15-5.
CRM 限界上下文作为事件源域模型实现。当 CRM 系统必须与营销限界上下文集成时,团队决定利用事件源数据模型的灵活性,让消费者(在本例中为营销)订阅 CRM 的领域事件并使用它们来投影模型符合他们的需要。
The CRM bounded context is implemented as an event-sourced domain model. When the CRM system had to be integrated with the Marketing bounded context, the teams decided to leverage the event-sourced data model’s flexibility and let the consumer—in this case, Marketing—subscribe to the CRM’s domain events and use them to project the model that fits their needs.
引入 AdsOptimization 限界上下文时,它还必须处理 CRM 限界上下文生成的信息。同样,团队决定让 AdsOptimization 订阅 CRM 中生成的所有域事件,并投影适合 AdsOptimization 需求的模型。
When the AdsOptimization bounded context was introduced, it also had to process the information produced by the CRM bounded context. Again, the teams decided to let AdsOptimization subscribe to all domain events produced in the CRM and project the model that fits AdsOptimization’s needs.
有趣的是,Marketing 和 AdsOptimization 限界上下文都必须以相同的格式呈现客户信息,因此最终从 CRM 的领域事件中投射出相同的模型:每个客户状态的平面快照。
Interestingly, both the Marketing and AdsOptimization bounded contexts had to present the customers’ information in the same format, and hence ended up projecting the same model out of the CRM’s domain events: a flattened snapshot of each customer’s state.
报告限界上下文仅订阅 CRM 发布的域事件的子集,并用作事件通知消息以获取在 AdsOptimization 上下文中执行的计算。但是,由于两个 AdsOptimization 限界上下文使用相同的事件来触发它们的计算,为确保更新报告模型,AdsOptimization 上下文引入了延迟。它在收到消息后五分钟处理消息。
The Reporting bounded context subscribed only to a subset of domain events published by the CRM and used as event notification messages to fetch the calculations performed in the AdsOptimization context. However, since both AdsOptimization bounded contexts use the same events to trigger their calculations, to ensure that the Reporting model is updated the AdsOptimization context introduced a delay. It processed messages five minutes after receiving them.
这个设计很糟糕。我们来分析一下这个系统中的耦合类型。
This design is terrible. Let’s analyze the types of coupling in this system.
AdsOptimization 和 Reporting 有界上下文是暂时的耦合:它们依赖于严格的执行顺序。AdsOptimization 组件必须在报告模块被触发之前完成其处理。如果顺序颠倒,Reporting系统会产生不一致的数据。
The AdsOptimization and Reporting bounded contexts are temporally coupled: they depend on a strict order of execution. The AdsOptimization component has to finish its processing before the Reporting module is triggered. If the order is reversed, inconsistent data will be produced in the Reporting system.
为了强制执行所需的执行顺序,工程师在报告系统中引入了处理延迟。这五分钟的延迟让 AdsOptimization 组件完成所需的计算。显然,这并不能防止错误的执行顺序:
To enforce the required execution order, the engineers introduced the processing delay in the Reporting system. This delay of five minutes lets the AdsOptimization component finish the required calculations. Obviously, this doesn’t prevent incorrect order of execution:
AdsOptimization 可能超载,无法在五分钟内完成处理。
AdsOptimization may be overloaded and unable to finish the processing in five minutes.
网络问题可能会延迟传入消息到 AdsOptimization 服务的传递。
A network issue may delay the delivery of incoming messages to the AdsOptimization service.
AdsOptimization 组件可能会遇到中断并停止处理传入消息。
The AdsOptimization component can experience an outage and stop processing incoming messages.
Marketing 和 AdsOptimization 限界上下文均已订阅到 CRM 的域事件,并最终实现了对客户数据的相同投影。换句话说,将传入的域事件转换为基于状态的表示的业务逻辑在两个限界上下文中都是重复的,并且更改的原因相同:它们必须以相同的格式呈现客户的数据。因此,如果在其中一个组件中更改了投影,则必须在第二个限界上下文中复制更改。
The Marketing and AdsOptimization bounded contexts both subscribed to the CRM’s domain events and ended up implementing the same projection of the customers’ data. In other words, the business logic that transforms incoming domain events into a state-based representation was duplicated in both bounded contexts, and it had the same reasons for change: they had to present the customers’ data in the same format. Therefore, if the projection was changed in one of the components, the change had to be replicated in the second bounded context.
这是功能耦合的一个例子:多个组件实现相同的业务功能,如果它发生变化,两个组件必须同时发生变化。
That’s an example of functional coupling: multiple components implementing the same business functionality, and if it changes, both components have to change simultaneously.
这种类型的耦合更加微妙。营销和广告优化限界上下文订阅了 CRM 的事件源模型生成的所有领域事件。因此,CRM 实现中的更改(例如添加新的领域事件或更改现有事件的模式)必须反映在两个订阅的限界上下文中!否则可能会导致数据不一致。例如,如果事件的架构发生变化,订阅者的投影逻辑就会失败。另一方面,如果将新的领域事件添加到 CRM 的模型中,它可能会影响预测的模型,因此忽略它会导致预测不一致的状态。
This type of coupling is more subtle. The Marketing and AdsOptimization bounded contexts are subscribed to all the domain events generated by the CRM’s event-sourced model. Consequently, a change in the CRM’s implementation, such as adding a new domain event or changing the schema of an existing one, has to be reflected in both subscribing bounded contexts! Failing to do so can lead to inconsistent data. For example, if an event’s schema changes, the subscribers’ projection logic will fail. On the other hand, if a new domain event is added to the CRM’s model, it can potentially affect the projected models, and thus, ignoring it will lead to projecting an inconsistent state.
如您所见,盲目地将事件注入系统既不会解耦也没有弹性。您可能会认为这是一个不切实际的例子,但不幸的是,这个例子是基于一个真实的故事。让我们看看如何调整事件以显着改进设计。
As you can see, blindly pouring events on a system makes it neither decoupled nor resilient. You may assume that this is an unrealistic example, but unfortunately, this example is based on a true story. Let’s see how the events can be adjusted to improve the design dramatically.
公开构成 CRM 数据模型的所有领域事件将订阅者与生产者的实现细节联系起来。可以通过公开更多受限事件集或不同类型的事件来解决实现耦合问题。
Exposing all the domain events constituting the CRM’s data model couples the subscribers to the producer’s implementation details. The implementation coupling can be addressed by exposing either a much more restrained set of events or a different type of events.
Marketing 和 AdsOptimization 订阅者通过实现相同的业务功能在功能上相互耦合。
The Marketing and AdsOptimization subscribers are functionally coupled to each other by implementing the same business functionality.
实现和功能耦合都可以通过在生产者中封装投影逻辑来解决:CRM 限界上下文。CRM 可以遵循消费者驱动的契约模式,而不是公开其实施细节:投射消费者所需的模型,并使其成为限界上下文的已发布语言的一部分——一种特定于集成的模型,与内部实施模型分离。结果,消费者获得了他们需要的所有数据,并且不知道 CRM 的实施模型。
Both implementation and functional coupling can be tackled by encapsulating the projection logic in the producer: the CRM bounded contexts. Instead of exposing its implementation details, the CRM can follow the consumer-driven contract pattern: project the model needed by the consumers and make it a part of the bounded context’s published language—an integration-specific model, decoupled from the internal implementation model. As a result, the consumers get all the data they need and are not aware of the CRM’s implementation model.
为了解决 AdsOptimization 和 Reporting 限界上下文之间的时间耦合,AdsOptimization 组件可以发布事件通知消息,触发 Reporting 组件获取它需要的数据。这个重构的系统如图 15-6所示。
To tackle the temporal coupling between the AdsOptimization and Reporting bounded contexts, the AdsOptimization component can publish an event notification message, triggering the Reporting component to fetch the data it needs. This refactored system is shown in Figure 15-6.
将事件类型与手头的任务相匹配,可以使最终设计的耦合度降低几个数量级,更加灵活和容错。让我们制定应用更改背后的设计启发式。
Matching types of events to the tasks at hand makes the resultant design orders of magnitude less coupled, more flexible, and fault tolerant. Let’s formulate the design heuristics behind the applied changes.
正如安德鲁·格罗夫所说,只有偏执狂存活。3在设计事件驱动系统时将此作为指导原则:
As Andrew Grove put it, only the paranoid survive.3 Use this as a guiding principle when designing event-driven systems:
网络会很慢。
The network is going to be slow.
服务器会在最不方便的时候出现故障。
Servers will fail at the most inconvenient moment.
事件将无序到达。
Events will arrive out of order.
事件将被复制。
Events will be duplicated.
最重要的是,这些事件最常发生在周末和公共假期。
Most importantly, these events will occur most frequently on weekends and public holidays.
事件驱动架构中的驱动一词意味着您的整个系统取决于消息的成功传递。因此,避免像瘟疫一样的“一切都会好起来”的心态。确保事件始终如一地交付,无论如何:
The word driven in event-driven architecture means your whole system depends on successful delivery of the messages. Hence, avoid the “things will be okay” mindset like the plague. Ensure that the events are always delivered consistently, no matter what:
使用发件箱模式可靠地发布消息。
Use the outbox pattern to publish messages reliably.
发布消息时,确保订阅者能够对消息进行重复数据删除,并识别和重新排序乱序的消息。
When publishing messages, ensure that the subscribers will be able to deduplicate the messages and identify and reorder out-of-order messages.
在编排需要发布补偿操作的跨界上下文流程时,利用 saga 和流程管理器模式。
Leverage the saga and process manager patterns when orchestrating cross-bounded context processes that require issuing compensating actions.
发布时要小心暴露实现细节域事件,尤其是在事件源聚合中。将事件视为限界上下文公共接口的固有部分。因此,在实现开放主机服务模式时,确保事件反映在限界上下文的发布语言中。第 9 章讨论了转换基于事件的模型的模式。
Be wary of exposing implementation details when publishing domain events, especially in event-sourced aggregates. Treat events as an inherent part of the bounded context’s public interface. Therefore, when implementing the open-host service pattern, ensure that the events are reflected in the bounded context’s published language. Patterns for transforming event-based models are discussed in Chapter 9.
在设计限界上下文的公共接口时,利用不同类型的事件。带有事件的状态传输消息将实现模型压缩为更紧凑的模型,该模型仅传达消费者需要的信息。
When designing bounded contexts’ public interfaces, leverage the different types of events. Event-carried state transfer messages compress the implementation model into a more compact model that communicates only the information the consumers need.
事件通知消息可用于进一步最小化公共接口。
Event notification messages can be used to further minimize the public interface.
最后,谨慎使用域事件与外部有界上下文进行通信。考虑设计一组专门的公共领域活动。
Finally, sparingly use domain events for communication with external bounded contexts. Consider designing a set of dedicated public domain events.
在设计事件驱动的通信时,评估限界上下文的一致性要求作为选择事件类型的附加启发式:
When designing event-driven communication, evaluate the bounded contexts’ consistency requirements as an additional heuristic for choosing the event type:
如果组件可以接受最终一致的数据,请使用事件携带的状态传输消息。
If the components can settle for eventually consistent data, use the event-carried state transfer message.
如果消费者需要读取生产者状态中的最后一次写入,则发出事件通知消息,随后查询以获取生产者的最新状态。
If the consumer needs to read the last write in the producer’s state, issue an event notification message, with a subsequent query to fetch the producer’s up-to-date state.
本章介绍了事件驱动架构作为设计有界上下文公共接口的固有方面。您了解了可用于跨界上下文通信的三种事件类型:
This chapter presented event-driven architecture as an inherent aspect of designing a bounded context’s public interface. You learned the three types of events that can be used for cross-bounded context communication:
使用不适当的事件类型会使基于 EDA 的系统脱轨,无意中将其变成一个大泥球。要为集成选择正确的事件类型,请评估限界上下文的一致性要求并注意公开实施细节。设计一组明确的公共和私人事件。最后,确保系统即使在遇到技术问题和中断时也能传送消息。
Using inappropriate types of events will derail an EDA-based system, inadvertently turning it into a big ball of mud. To choose the correct type of events for integration, evaluate the bounded contexts’ consistency requirements and be wary of exposing implementation details. Design an explicit set of public and private events. Finally, ensure that the system delivers the messages, even in the face of technical issues and outages.
以下哪些陈述是正确的?
事件驱动架构定义了旨在跨越组件边界的事件。
事件溯源定义了旨在保留在有界上下文边界内的事件。
事件驱动架构和事件溯源是同一模式的不同术语。
A和B是正确的。
Which of the following statements is/are correct?
Event-driven architecture defines the events intended to travel across components’ boundaries.
Event sourcing defines the events that are intended to stay within the bounded context’s boundary.
Event-driven architecture and event sourcing are different terms for the same pattern.
A and B are correct.
什么类型的事件最适合传达状态变化?
事件通知。
事件携带的状态转移。
域事件。
所有事件类型都同样适用于传达状态变化。
What type of event is best suited for communicating changes in state?
Event notification.
Event-carried state transfer.
Domain event.
All event types are equally good for communicating changes in state.
哪种限界上下文集成模式需要显式定义公共事件?
开放主机服务
反腐层
共享内核
墨守成规
Which bounded context integration pattern calls for explicitly defining public events?
Open-host service
Anticorruption layer
Shared kernel
Conformist
服务 S1 和 S2 是异步集成的。S1 必须进行数据通信,S2 需要能够读取 S1 中最后写入的数据。哪种类型的事件适合这种集成场景?
S2 应该发布事件携带的状态转移事件。
S2 应该发布公共事件通知,通知 S1 发出同步请求以获取最新信息。
S2 应该发布域事件。
A和B。
The services S1 and S2 are integrated asynchronously. S1 has to communicate data and S2 needs to be able to read the last written data in S1. Which type of event fits this integration scenario?
S2 should publish event-carried state transfer events.
S2 should publish public event notifications, which will signal S1 to issue a synchronous request to get the most up-to-date information.
S2 should publish domain events.
A and B.
1个Hohpe, G., & Woolf, B. (2003)。企业集成模式:设计、构建和部署消息传递解决方案。波士顿:Addison-Wesley。
1 Hohpe, G., & Woolf, B. (2003). Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Boston: Addison-Wesley.
2个Fowler, M.(日期不详)。“事件驱动”是什么意思?2021 年 8 月 12 日从Martin Fowler(博客)检索。
2 Fowler, M. (n.d.). What do you mean by “Event-Driven”? Retrieved August 12, 2021, from Martin Fowler (blog).
3个格罗夫 (1998)。只有偏执狂才能生存。伦敦:HarperCollins Business。
3 Grove, A. S. (1998). Only the Paranoid Survive. London: HarperCollins Business.
到目前为止,在本书中,我们已经讨论了用于构建操作系统的模型。操作系统实施实时事务处理系统的数据并协调其与环境的日常交互。这些模型是联机事务处理 (OLTP) 数据。另一种值得关注和适当建模的数据是联机分析处理 (OLAP) 数据。
So far in this book, we have discussed models used to build operational systems. Operational systems implement real-time transactions that manipulate the system’s data and orchestrate its day-to-day interactions with its environment. These models are the online transactional processing (OLTP) data. Another type of data that deserves attention and proper modeling is online analytical processing (OLAP) data.
在本章中,您将了解称为数据网格的分析数据管理架构。您将看到基于数据网格的架构如何工作,以及它与更传统的 OLAP 数据管理方法有何不同。最终,您将看到领域驱动设计和数据网格如何相互适应。但首先,让我们看看这些分析数据模型是什么,以及为什么我们不能只为分析用例重用操作模型。
In this chapter, you will learn about the analytical data management architecture called data mesh. You will see how the data mesh–based architecture works and how it differs from the more traditional OLAP data management approaches. Ultimately, you will see how domain-driven design and data mesh accommodate each other. But first, let’s see what these analytical data models are and why we can’t just reuse the operational models for analytical use cases.
他们说知识就是力量。分析数据就是知识这使公司能够利用积累的数据深入了解如何优化业务、更好地了解客户需求,甚至通过训练机器学习 (ML) 模型做出自动化决策。
They say knowledge is power. Analytical data is the knowledge that gives companies the power to leverage accumulated data to gain insights into how to optimize the business, better understand customers’ needs, and even make automated decisions by training machine learning (ML) models.
分析模型(OLAP)和操作模型(OLTP)服务于不同类型的消费者,实现不同类型的用例,因此设计遵循其他设计原则。
The analytical models (OLAP) and operational models (OLTP) serve different types of consumers, enable the implementation of different kinds of use cases, and are therefore designed following other design principles.
操作模型围绕系统业务领域的各种实体构建,实现它们的生命周期并协调它们之间的交互。如图 16-1所示,这些模型服务于操作系统,因此必须对其进行优化以支持实时业务交易。
Operational models are built around the various entities from the system’s business domain, implementing their lifecycles and orchestrating their interactions with one another. These models, depicted in Figure 16-1, are serving operational systems and hence have to be optimized to support real-time business transactions.
分析模型旨在提供对操作系统的不同见解。分析模型不是实施实时交易,而是旨在提供对业务活动绩效的洞察力,更重要的是,提供业务如何优化其运营以实现更大价值的洞察力。
Analytical models are designed to provide different insights into the operational systems. Instead of implementing real-time transactions, an analytical model aims to provide insights into the performance of business activities and, more importantly, how the business can optimize its operations to achieve greater value.
从数据结构的角度来看,OLAP 模型忽略了单个业务实体,而是通过对事实表和维度表进行建模来关注业务活动。接下来我们将仔细研究这些表中的每一个。
From a data structure perspective, OLAP models ignore the individual business entities and instead focus on business activities by modeling fact tables and dimension tables. We’ll take a closer look at each of these tables next.
事实代表已经发生的业务活动。事实类似于领域事件的概念,两者都描述了过去发生的事情。然而,与领域事件相反,没有文体要求将事实命名为过去时的动词。不过,事实代表业务流程的活动。例如,事实表Fact_CustomerOnboardings将包含每个新入职客户的记录和Fact_Sales每个承诺销售的记录。图 16-2显示了事实表的示例。
Facts represent business activities that have already happened. Facts are similar to the notion of domain events in the sense that both describe things that happened in the past. However, contrary to domain events, there is no stylistic requirement to name facts as verbs in the past tense. Still, facts represent activities of business processes. For example, a fact table Fact_CustomerOnboardings would contain a record for each new onboarded customer and Fact_Sales a record for each committed sale. Figure 16-2 shows an example of a fact table.
此外,与领域事件类似,事实记录永远不会被删除或修改:分析数据是仅附加数据:表示当前数据已过时的唯一方法是附加具有当前状态的新记录。考虑图 16-3Fact_CaseStatus中的事实表。它包含随时间变化的支持请求状态的度量。事实名称中没有明确的动词,但事实捕获的业务流程是处理支持案例的流程。
Also, similar to domain events, fact records are never deleted or modified: analytical data is append-only data: the only way to express that current data is outdated is to append a new record with the current state. Consider the fact table Fact_CaseStatus in Figure 16-3. It contains the measurements of the statuses of support requests through time. There is no explicit verb in the fact name, but the business process captured by the fact is the process of taking care of support cases.
OLAP 和 OLTP 模型之间的另一个显着区别是数据的粒度。操作系统需要最精确的数据来处理业务交易。对于分析模型,聚合数据在许多用例中更有效。例如,在图 16-3Fact_CaseStatus所示的表中,您可以看到每 30 分钟拍摄一次快照。使用模型的数据分析师决定最适合他们需求的粒度级别。为测量的每次更改创建一个事实记录——例如,案例数据的每次更改——在某些情况下会造成浪费,而在其他情况下甚至在技术上是不可能的。
Another significant difference between the OLAP and OLTP models is the granularity of the data. Operational systems require the most precise data to handle business transactions. For analytical models, aggregated data is more efficient in many use cases. For example, in the Fact_CaseStatus table shown in Figure 16-3, you can see that the snapshots are taken every 30 minutes. The data analysts working with the model decide what level of granularity will best suit their needs. Creating a fact record for each change of the measurement—for example, each change of a case’s data—would be wasteful in some cases and even technically impossible in others.
分析模型的另一个重要组成部分是一个维度。如果事实表示业务流程或操作(动词),则维度描述事实(形容词)。
Another essential building block of an analytical model is a dimension. If a fact represents a business process or action (a verb), a dimension describes the fact (an adjective).
维度旨在描述事实的属性,并作为从事实表到维度表的外键引用。建模为维度的属性是在不同事实记录中重复且无法放入单个列中的任何测量值或数据。例如,图 16-4SolvedCases中的模式用它的维度扩充了事实。
The dimensions are designed to describe the facts’ attributes and are referenced as a foreign key from a fact table to a dimension table. The attributes modeled as dimensions are any measurements or data that is repeated across different fact records and cannot fit in a single column. For example, the schema in Figure 16-4 augments the SolvedCases fact with its dimensions.
维度高度归一化的原因是分析系统需要支持灵活的查询。这是运营模型和分析模型之间的另一个区别。可以预测将如何查询操作模型以支持业务需求。分析模型的查询模式是不可预测的。数据分析师需要灵活的方式来查看数据,并且很难预测将来会执行哪些查询。因此,规范化支持动态查询和过滤,以及跨不同维度对事实数据进行分组。
The reason for the high normalization of the dimensions is the analytical system’s need to support flexible querying. That’s another difference between operational and analytical models. It’s possible to predict how an operational model will be queried to support the business requirements. The querying patterns of the analytical models are not predictable. The data analysts need flexible ways of looking at the data, and it’s hard to predict what queries will be executed in the future. As a result, the normalization supports dynamic querying and filtering, and grouping the facts data across the different dimensions.
图 16-5中描述的表结构称为星型模式。它基于事实与其维度之间的多对一关系:每个维度记录被许多事实使用;事实的外键指向单个维度记录。
The table structure depicted in Figure 16-5 is called the star schema. It is based on the many-to-one relationships between the facts and their dimensions: each dimension record is used by many facts; a fact’s foreign key points to a single dimension record.
另一个主要的分析模型是雪花模式。雪花模式基于相同的构建块:事实和维度。然而,在雪花模式中,维度是多级的:每个维度进一步归一化为更细粒度的维度,如图16-6所示。
Another predominant analytical model is the snowflake schema. The snowflake schema is based on the same building blocks: facts and dimensions. However, in the snowflake schema, the dimensions are multilevel: each dimension is further normalized into more fine-grained dimensions, as shown in Figure 16-6.
由于额外的规范化,雪花模式将使用更少的空间来存储维度数据并且更易于维护。但是,查询事实的数据需要连接更多的表,因此需要更多的计算资源。
As a result of the additional normalization, the snowflake schema will use less space to store the dimension data and is easier to maintain. However, querying the facts’ data will require joining more tables, and therefore, more computational resources are needed.
星型和雪花模式都允许数据分析师分析业务绩效,深入了解可以优化和构建到商业智能 (BI) 报告中的内容。
Both the star and snowflake schemas allow data analysts to analyze business performance, gaining insights into what can be optimized and built into business intelligence (BI) reports.
让我们将讨论从分析建模转移到数据管理支持生成和提供分析数据的架构。在本节中,我们将讨论两种常见的分析数据架构:数据仓库和数据湖。您将了解每种架构的基本工作原理、它们之间的区别以及每种方法的挑战。了解这两种架构如何工作将为讨论本章的主题奠定基础:数据网格范式及其与领域驱动设计的相互作用。
Let’s shift the discussion from analytical modeling to data management architectures that support generating and serving analytical data. In this section, we will discuss two common analytical data architectures: data warehouse and data lake. You will learn the basic working principles of each architecture, how they differ from each other, and the challenges of each approach. Knowledge of how the two architectures work will build the foundation for discussing the main topic of this chapter: the data mesh paradigm and its interplay with domain-driven design.
数据仓库 (DWH) 架构相对简单。从企业的所有运营系统中提取数据,将源数据转换为分析模型,并将结果数据加载到面向数据分析的数据库中。这个数据库就是数据仓库。
The data warehouse (DWH) architecture is relatively straightforward. Extract data from all of the enterprise’s operational systems, transform the source data into an analytical model, and load the resultant data into a data analysis–oriented database. This database is the data warehouse.
这种数据管理架构主要基于提取-转换-加载 (ETL) 脚本。数据可以来自各种来源:操作数据库、流式事件、日志等。除了将源数据转换为基于事实/维度的模型外,转换步骤还可能包括其他操作,例如删除敏感数据、删除重复记录、重新排序事件、聚合细粒度事件等。在某些情况下,转换可能需要对传入数据进行临时存储。这被称为暂存区。
This data management architecture is based primarily on the extract-transform-load (ETL) scripts. The data can come from various sources: operational databases, streaming events, logs, and so on. In addition to translating the source data into a facts/dimensions-based model, the transformation step may include additional operations such as removing sensitive data, deduplicating records, reordering events, aggregating fine-grained events, and more. In some cases, the transformation may require temporary storage for the incoming data. This is known as the staging area.
结果数据仓库如图16-7所示,包含涵盖企业所有业务流程的分析数据。数据使用 SQL 语言(或其一种方言)公开,并由数据分析师和 BI 工程师使用。
The resultant data warehouse, shown in Figure 16-7, contains analytical data covering all of the enterprise’s business processes. The data is exposed using the SQL language (or one of its dialects) and is used by data analysts and BI engineers.
细心的读者会注意到数据仓库体系结构与第 2章和第 3章中讨论的一些挑战相同。
The careful reader will notice that the data warehouse architecture shares some of the challenges discussed in Chapters 2 and 3.
首先,数据仓库架构的核心是构建企业范围的模型。该模型应描述企业所有系统生成的数据,并解决分析数据的所有不同用例。例如,分析模型可以优化业务、降低运营成本、制定智能业务决策、报告,甚至培训 ML 模型。正如您在第 3 章中了解到的,这种方法对于最小的组织来说是不切实际的。为手头的任务设计模型,例如构建报告或训练 ML 模型,是一种更有效和可扩展的方法。
First, at the heart of the data warehouse architecture is the goal of building an enterprise-wide model. The model should describe the data produced by all of the enterprise’s systems and address all of the different use cases for analytical data. The analytical model enables, for example, optimizing the business, reducing operational costs, making intelligent business decisions, reporting, and even training ML models. As you learned in Chapter 3, such an approach is impractical for anything by the smallest organizations. Designing a model for the task at hand, such as building reports or training ML models, is a much more effective and scalable approach.
使用数据集市可以部分解决构建包罗万象的模型的挑战。数据集市是一个数据库,它保存与明确定义的分析需求相关的数据,例如单个业务部门的分析。在图 16-8所示的数据集市模型中,一个集市由来自操作系统的 ETL 过程直接填充,而另一个集市从数据仓库中提取数据。
The challenge of building an all-encompassing model can be partly addressed by the use of data marts. A data mart is a database that holds data relevant for well-defined analytical needs, such as analysis of a single business department. In the data mart model shown in Figure 16-8, one mart is populated directly by an ETL process from an operational system, while another mart extracts its data from the data warehouse.
当数据从企业数据仓库进入数据集市时,仍然需要在数据仓库中定义企业范围的模型。或者,数据集市可以实施专用的 ETL 流程,以直接从操作系统中获取数据。在这种情况下,生成的模型使得跨不同集市(例如,跨不同部门)查询数据变得具有挑战性,因为它需要跨数据库查询并显着影响性能。
When the data is ingested into a data mart from the enterprise data warehouse, the enterprise-wide model still needs to be defined in the data warehouse. Alternatively, data marts can implement dedicated ETL processes to ingest data directly from the operational systems. In this case, the resultant model makes it challenging to query data across different marts—for example, across different departments—as it requires a cross-database query and significantly impacts performance.
数据仓库架构的另一个具有挑战性的方面是 ETL 过程在分析 (OLAP) 和操作 (OLTP) 系统之间创建了强耦合。ETL 脚本使用的数据不一定通过系统的公共接口公开。通常,DWH 系统只是获取驻留在操作系统数据库中的所有数据。操作数据库中使用的模式不是公共接口,而是内部实现细节。因此,架构中的微小变化注定会破坏数据仓库的 ETL 脚本。由于运营和分析系统是由距离较远的组织单位实施和维护的,因此两者之间的沟通具有挑战性,并导致团队之间产生很多摩擦。这种通信模式如图 16-9所示。
Another challenging aspect of the data warehouse architecture is that the ETL processes create a strong coupling between the analytical (OLAP) and the operational (OLTP) systems. The data consumed by the ETL scripts is not necessarily exposed through the system’s public interfaces. Often, DWH systems simply fetch all the data residing in the operational systems’ databases. The schema used in the operational database is not a public interface, but rather an internal implementation detail. As a result, a slight change in the schema is destined to break the data warehouse’s ETL scripts. Since the operational and analytical systems are implemented and maintained by somewhat distant organizational units, the communication between the two is challenging and leads to lots of friction between the teams. This communication pattern is shown in Figure 16-9.
数据湖架构解决了数据仓库架构的一些缺点。
The data lake architecture addresses some of the shortcomings of the data warehouse architecture.
作为数据仓库,数据湖架构基于相同的摄取操作系统数据并将其转换为分析模型的概念。但是,这两种方法在概念上存在差异。
As a data warehouse, the data lake architecture is based on the same notion of ingesting the operational systems’ data and transforming it into an analytical model. However, there is a conceptual difference between the two approaches.
基于数据湖的系统摄取操作系统的数据。但是,数据不会立即转换为分析模型,而是以其原始形式(即原始操作模型)持久保存。
A data lake–based system ingests the operational systems’ data. However, instead of being transformed right away into an analytical model, the data is persisted in its raw form, that is, in the original operational model.
最终,原始数据无法满足数据分析师的需求。因此,数据工程师和 BI 工程师的工作是理解湖中的数据并实施 ETL 脚本,这些脚本将生成分析模型并将它们输入数据仓库。图 16-10描绘了一个数据湖架构。
Eventually, the raw data cannot fit the needs of data analysts. As a result, it is the job of the data engineers and the BI engineers to make sense of the data in the lake and implement the ETL scripts that will generate analytical models and feed them into a data warehouse. Figure 16-10 depicts a data lake architecture.
由于操作系统的数据以其原始的原始形式保存,并且仅在之后进行转换,因此数据湖允许使用多个面向任务的分析模型。一个模型可用于报告,另一个用于训练 ML 模型,等等。此外,将来可以添加新模型并使用现有原始数据进行初始化。
Since the operational systems’ data is persisted in its original, raw form and is transformed only afterward, the data lake allows working with multiple, task-oriented analytical models. One model can be used for reporting, another for training ML models, and so on. Furthermore, new models can be added in the future and initialized with the existing raw data.
也就是说,分析模型的延迟生成增加了整个系统的复杂性。数据工程师实现和支持同一 ETL 脚本的多个版本以适应不同版本的操作模型的情况并不少见,如图16-11所示。
That said, the delayed generation of analytical models increases the complexity of the overall system. It’s not uncommon for data engineers to implement and support multiple versions of the same ETL script to accommodate different versions of the operational model, as shown in Figure 16-11.
此外,由于数据湖是无模式的——没有强加于传入数据的模式——并且无法控制传入数据的质量,因此数据湖的数据在一定规模上变得混乱。数据湖使摄取数据变得容易,但使用数据却更具挑战性。或者,正如人们常说的那样,数据湖变成了数据沼泽。为了理解混乱并提取有用的分析数据,数据科学家的工作变得复杂了几个数量级。
Furthermore, since data lakes are schema-less—there is no schema imposed on the incoming data—and there is no control over the quality of the incoming data, the data lake’s data becomes chaotic at certain levels of scale. Data lakes make it easy to ingest data but much more challenging to make use of it. Or, as is often said, a data lake becomes a data swamp. The data scientist’s job becomes orders of magnitude more complex to make sense of the chaos and to extract useful analytical data.
数据仓库和数据湖架构都基于这样的假设为分析而摄取的数据越多,组织获得的洞察力就越多。然而,这两种方法都倾向于在“大”数据的重压下崩溃。从操作模型到分析模型的转换汇聚成数以千计的大规模不可维护的临时 ETL 脚本。
Both data warehouse and data lake architectures are based on the assumption that the more data that is ingested for analytics, the more insight the organization will gain. Both approaches, however, tend to break under the weight of “big” data. The transformation of operational to analytical models converges to thousands of unmaintainable, ad hoc ETL scripts at scale.
从建模的角度来看,这两种体系结构都跨越了操作系统的边界,并在其实现细节上产生了依赖性。由此产生的与实施模型的耦合会在操作和分析系统团队之间产生摩擦,通常会为了不破坏分析系统的 ETL 工作而阻止更改操作模型。
From a modeling perspective, both architectures trespass the boundaries of the operational systems and create dependencies on their implementation details. The resultant coupling to the implementation models creates friction between the operational and analytical systems teams, often to the point of preventing changes to the operational models for the sake of not breaking the analysis system’s ETL jobs.
更糟糕的是,由于数据分析师和数据工程师属于一个独立的组织单位,他们往往缺乏对操作系统开发团队所拥有的业务领域的深入了解。他们主要专注于大数据工具,而不是业务领域的知识。
To make matters worse, since the data analysts and data engineers belong to a separate organizational unit, they often lack the deep knowledge of the business domain possessed by the operational systems’ development teams. Instead of the knowledge of the business domain, they are specialized mainly in big data tooling.
最后但并非最不重要的一点是,在基于领域驱动设计的项目中,与实施模型的耦合尤为严重,在这些项目中,重点是不断发展和改进业务领域的模型。因此,操作模型的更改可能会对分析模型产生无法预料的后果。这种变化在 DDD 项目中很常见,并且经常导致研发和数据团队之间的摩擦。
Last but not least, the coupling to the implementation models is especially acute in domain-driven design–based projects, in which the emphasis is on continuously evolving and improving the business domain’s models. As a result, a change in the operational model can have unforeseen consequences in the analytical model. Such changes are frequent in DDD projects and often result in friction between R&D and data teams.
数据仓库和数据湖的这些局限性激发了一种新的分析数据管理架构:数据网格。
These limitations of data warehouses and data lakes inspired a new analytical data management architecture: data mesh.
从某种意义上说,数据网格架构是分析数据的领域驱动设计。由于 DDD 的不同模式划分边界并保护其内容,数据网格架构定义和保护分析数据的模型和所有权边界。
The data mesh architecture is, in a sense, domain-driven design for analytical data. As the different patterns of DDD draw boundaries and protect their contents, the data mesh architecture defines and protects model and ownership boundaries for analytical data.
数据网格架构基于四个核心原则:围绕域分解数据、数据作为产品、实现自治和构建生态系统。让我们详细讨论每个原则。
The data mesh architecture is based on four core principles: decompose data around domains, data as a product, enable autonomy, and build an ecosystem. Let’s discuss each principle in detail.
数据仓库和数据湖方法都旨在统一所有将企业的数据整合成一个大模型。由于与企业范围内的运营模型相同的原因,由此产生的分析模型是无效的。此外,将所有系统的数据收集到一个位置模糊了各种数据元素的所有权界限。
Both the data warehouse and data lake approaches aim to unify all of the enterprise’s data into one big model. The resultant analytical model is ineffective for all the same reasons as an enterprise-wide operational model is. Furthermore, gathering data from all systems into one location blurs the ownership boundaries of the various data elements.
数据网格架构不是构建单一的分析模型要求利用我们在第 3 章中针对运营数据讨论的相同解决方案:使用多个分析模型并将它们与数据来源对齐。这自然地将分析模型的所有权边界与限界上下文的边界对齐,如图16-12所示。当分析模型根据系统的限界上下文分解后,分析数据的生成就变成了相应产品团队的责任。
Instead of building a monolithic analytical model, the data mesh architecture calls for leveraging the same solution we discussed in Chapter 3 for operational data: use multiple analytical models and align them with the origin of the data. This naturally aligns the ownership boundaries of the analytical models with the bounded contexts’ boundaries, as shown in Figure 16-12. When the analysis model is decomposed according to the system’s bounded contexts, the generation of the analysis data becomes the responsibility of the corresponding product teams.
每个限界上下文现在都拥有其操作 (OLTP) 和分析 (OLAP) 模型。因此,同一个团队拥有运营模型,现在负责将其转换为分析模型。
Each bounded context now owns its operational (OLTP) and analytical (OLAP) models. Consequently, the same team owns the operational model, now in charge of transforming it into the analytical model.
经典的数据管理架构很难发现,理解并获取高质量的分析数据。这在数据湖的情况下尤其严重。
The classic data management architectures make it difficult to discover, understand, and fetch quality analytical data. This is especially acute in the case of data lakes.
数据作为产品原则要求处理分析数据作为一等公民。分析系统不必从可疑来源(内部数据库、日志文件等)获取操作数据,在基于数据网格的系统中,有界上下文通过明确定义的输出端口为分析数据提供服务,如图 16所示-13。
The data as a product principle calls for treating the analytical data as a first-class citizen. Instead of the analytical systems having to get the operational data from dubious sources (internal database, logfiles, etc.), in a data mesh–based system the bounded contexts serve the analytical data through well-defined output ports, as shown in Figure 16-13.
分析数据应与任何公共 API 一样对待:
Analytical data should be treated the same as any public API:
发现必要的端点应该很容易:数据输出端口。
It should be easy to discover the necessary endpoints: the data output ports.
分析端点应该有一个定义明确的模式来描述所服务的数据及其格式。
The analytical endpoints should have a well-defined schema describing the served data and its format.
分析数据应该是可信的,并且与任何 API 一样,它应该定义和监控服务级别协议 (SLA)。
The analytical data should be trustworthy, and as with any API, it should have defined and monitored service-level agreements (SLAs).
分析模型应作为常规 API 进行版本控制,并相应地管理模型中破坏集成的更改。
The analytical model should be versioned as a regular API and correspondingly manage integration-breaking changes in the model.
此外,由于分析数据被视为产品,因此它必须满足消费者的需求。限界上下文的团队负责确保生成的模型满足其消费者的需求。与数据仓库和数据湖架构相反,对于数据网格,数据质量责任是一个顶级问题。
Furthermore, since the analytical data is treated as a product, it has to address the needs of its consumers. The bounded context’s team is in charge of ensuring that the resultant model addresses the needs of its consumers. Contrary to the data warehouse and data lake architectures, with data mesh, accountability for data quality is a top-level concern.
分布式数据管理架构的目标是允许结合细粒度的分析模型来解决组织的数据分析需求。例如,如果 BI 报告应该反映来自多个限界上下文的数据,它应该能够在需要时轻松获取它们的分析数据、应用本地转换并生成报告。
The goal of the distributed data management architecture is to allow the fine-grained analytical models to be combined to address the organization’s data analysis needs. For example, if a BI report should reflect data from multiple bounded contexts, it should be able to easily fetch their analytical data if needed, apply local transformations, and produce the report.
最后,不同的消费者可能需要不同形式的分析数据。有些人可能更喜欢执行 SQL 查询,其他人可能更喜欢从对象存储服务中获取分析数据,等等。因此,数据产品必须是多语言的,以适合不同消费者需求的格式提供数据。
Finally, different consumers may require the analytical data in different forms. Some may prefer to execute SQL queries, others to fetch analytical data from an object storage service, and so on. As a result, the data products have to be polyglot, serving the data in formats that suit different consumers’ needs.
为了将数据作为产品原则来实施,产品团队需要增加面向数据的专家。这是跨职能团队难题中缺失的部分,传统上只包括与操作系统相关的专家。
To implement the data as a product principle, product teams require adding data-oriented specialists. That’s the missing piece in the cross-functional teams puzzle, which traditionally includes only specialists related to the operational systems.
产品团队应该能够创建自己的数据产品并消费由其他限界上下文提供的数据产品。就像限界上下文一样,数据产品应该是可互操作的。
The product teams should be able to both create their own data products and consume data products served by other bounded contexts. Just as in the case of bounded contexts, the data products should be interoperable.
如果每个团队都构建自己的解决方案来提供分析数据,那将是浪费、低效且难以集成的。为了防止这种情况发生,需要一个平台来抽象构建、执行和维护可互操作数据产品的复杂性。设计和构建这样一个平台是一项艰巨的任务,需要专门的数据基础设施平台团队。
It would be wasteful, inefficient, and hard to integrate if each team builds their own solution for serving analytical data. To prevent this from happening, a platform is needed to abstract the complexity of building, executing, and maintaining interoperable data products. Designing and building such a platform is a considerable undertaking and requires a dedicated data infrastructure platform team.
数据基础设施平台团队应负责定义产品团队可以利用的数据产品蓝图、统一访问模式、访问控制和多语言存储,以及监控平台并确保满足 SLA 和目标。
The data infrastructure platform team should be in charge of defining the data product blueprints, unified access patterns, access control, and polyglot storage that can be leveraged by product teams, as well as monitoring the platform and ensuring that the SLAs and objectives are met.
创建数据网格系统的最后一步是指定一个联邦治理机构,以在分析数据领域实现互操作性和生态系统思维。通常,这将是一个由限界上下文的数据和产品所有者以及数据基础架构平台团队的代表组成的组,如图16-14所示。
The final step to creating a data mesh system is to appoint a federated governance body to enable interoperability and ecosystem thinking in the domain of the analytical data. Typically, that would be a group consisting of the bounded contexts’ data and product owners and representatives of the data infrastructure platform team, as shown in Figure 16-14.
治理小组负责定义规则以确保健康和可互操作的生态系统。这些规则必须应用于所有数据产品及其接口,确保整个企业遵守这些规则是团队的责任。
The governance group is in charge of defining the rules to ensure a healthy and interoperable ecosystem. The rules have to be applied to all data products and their interfaces, and it’s the group’s responsibility to ensure adherence to the rules throughout the enterprise.
这些是数据网格架构所基于的四个原则。强调定义边界,并将实现细节封装在定义明确的输出端口后面,这表明数据网格体系结构基于与域驱动设计相同的推理。此外,一些领域驱动的设计模式可以极大地支持数据网格架构的实现。
These are the four principles that the data mesh architecture is based on. The emphasis on defining boundaries, and encapsulating the implementation details behind well-defined output ports, makes it evident that the data mesh architecture is based on the same reasoning as domain-driven design. Furthermore, some of the domain-driven design patterns can greatly support implementing the data mesh architecture.
首先也是最重要的,无处不在的语言和由此产生的领域知识对于设计分析模型至关重要。正如我们在数据仓库和数据湖部分所讨论的那样,传统架构中缺乏领域知识。
First and foremost, the ubiquitous language and the resultant domain knowledge are essential for designing analytical models. As we discussed in the data warehouse and data lake sections, domain knowledge is lacking in traditional architectures.
其次,在不同的模型中公开有界上下文的数据从其运营模式来看,是开放主机模式。在这种情况下,分析模型是一种额外的已发布语言。
Second, exposing a bounded context’s data in a model that is different from its operational model is the open-host pattern. In this case, the analytical model is an additional published language.
CQRS 模式可以轻松生成相同数据的多个模型。它可用于将运营模型转换为分析模型。CQRS 模式从头开始生成模型的能力使得同时生成和提供多个版本的分析模型变得容易,如图16-15所示。
The CQRS pattern makes it easy to generate multiple models of the same data. It can be leveraged to transform the operational model into an analytical model. The CQRS pattern’s ability to generate models from scratch makes it easy to generate and serve multiple versions of the analytical model simultaneously, as shown in Figure 16-15.
最后,由于数据网格架构结合了不同的限界上下文模型来实现分析用例,因此操作模型的限界上下文集成模式也适用于分析模型。两个产品团队可以合作改进他们的分析模型。另一个可以实施反腐败层来保护自己免受无效分析模型的影响。或者,另一方面,团队可以分道扬镳,重复执行分析模型。
Finally, since the data mesh architecture combines the different bounded contexts’ models to implement analytical use cases, the bounded context integration patterns for operational models apply for analytical models as well. Two product teams can evolve their analytical models in partnership. Another can implement an anticorruption layer to protect itself from an ineffective analytical model. Or, on the other hand, the teams can go their separate ways and produce duplicate implementations of analytical models.
在本章中,您学习了设计软件系统的不同方面,特别是定义和管理分析数据。我们讨论了分析数据的主要模型,包括星型和雪花模式,以及传统上如何在数据仓库和数据湖中管理数据。
In this chapter, you learned the different aspects of designing software systems, in particular, defining and managing analytical data. We discussed the predominant models for analytical data, including the star and snowflake schemas, and how the data is traditionally managed in data warehouses and data lakes.
数据网格架构旨在应对传统数据管理架构的挑战。它的核心是将与领域驱动设计相同的原则应用于分析数据:将分析模型分解为可管理的单元,并确保可以通过其公共接口可靠地访问和使用分析数据。最终,CQRS 和限界上下文集成模式可以支持实现数据网格架构。
The data mesh architecture aims to address the challenges of the traditional data management architectures. At its core, it applies the same principles as domain-driven design but to analytical data: decomposing the analytical model into manageable units and ensuring that the analytical data can be reliably accessed and used through its public interfaces. Ultimately, the CQRS and bounded context integration patterns can support implementing the data mesh architecture.
关于事务 (OLTP) 和分析 (OLAP) 模型之间的差异,以下哪些陈述是正确的?
OLAP 模型应该公开比 OLTP模型更灵活的查询选项。
预计 OLAP 模型将比 OLTP 模型进行更多更新,因此必须针对写入进行优化。
OLTP 数据针对实时操作进行了优化,而等待 OLAP 查询响应几秒甚至几分钟也是可以接受的。
A和C正确。
Which of the following statements is/are correct regarding the differences between transactional (OLTP) and analytical (OLAP) models?
OLAP models should expose more flexible querying options than OLTP models.
OLAP models are expected to undergo more updates than OLTP models, and thus have to be optimized for writes.
OLTP data is optimized for real-time operations, whereas it’s acceptable to wait seconds or even minutes for an OLAP query’s response.
A and C are correct.
哪种限界上下文集成模式对于数据网格架构的实施至关重要?
共享内核
开放主机服务
反腐层
合伙
Which bounded context integration pattern is essential for implementation of the data mesh architecture?
Shared kernel
Open-host service
Anticorruption layer
Partnership
哪种架构模式对于数据网格架构的实施至关重要?
分层架构。
端口和适配器。
CQRS。
架构模式不能支持 OLAP 模型的实现。
Which architectural pattern is essential for implementation of the data mesh architecture?
Layered architecture.
Ports & adapters.
CQRS.
Architectural patterns cannot support implementation of an OLAP model.
数据网格架构的定义要求围绕“域”分解数据。DDD 表示数据网格域的术语是什么?
有界上下文。
业务领域。
子域。
DDD 中没有数据网格域的同义词。
The definition of data mesh architecture calls for decomposing data around “domains.” What is DDD’s term for denoting the data mesh’s domains?
Bounded contexts.
Business domains.
Subdomains.
There is no synonym for a data mesh’s domains in DDD.
为了完成我们对领域驱动设计的探索,我想回到我们开始的引述:
To complete our exploration of domain-driven design I want to get back to the quote we started with:
在我们就问题达成一致之前讨论解决方案是没有意义的,在我们就解决方案达成一致之前讨论实施步骤也是没有意义的。
Efrat Goldratt-Ashlag
There is no sense in talking about the solution before we agree on the problem, and no sense talking about the implementation steps before we agree on the solution.
Efrat Goldratt-Ashlag
这句话巧妙地总结了我们的 DDD 之旅。
This quote neatly summarizes our DDD journey.
要提供软件解决方案,我们首先必须了解问题:我们从事的业务领域是什么,业务目标是什么,实现这些目标的策略是什么。
To provide a software solution, we first have to understand the problem: what is the business domain that we are working in, what are the business goals, and what is the strategy for achieving them.
我们使用无处不在的语言来深入了解我们必须在软件中实现的业务领域及其逻辑。
We used the ubiquitous language to gain a deep understanding of the business domain and its logic that we have to implement in software.
您学会了通过将业务问题分解为有界上下文来管理业务问题的复杂性。每个限界上下文都实现了业务领域的单一模型,旨在解决特定问题。
You learned to manage the complexity of the business problem by breaking it apart into bounded contexts. Each bounded context implements a single model of the business domain, aimed at solving a specific problem.
我们讨论了如何识别和分类业务领域的构建块:核心、支持和通用子域。表 E-1比较了这三种类型的子域。
We discussed how to identify and categorize the building blocks of business domains: core, supporting, and generic subdomains. Table E-1 compares these three types of subdomains.
| 子域类型 | 竞争优势 | 复杂 | 挥发性 | 执行 | 问题 |
| 核 | 是的 | 高的 | 高的 | 内部 | 有趣的 |
| 通用的 | 不 | 高的 | 低的 | 购买/领养 | 解决了 |
| 配套 | 不 | 低的 | 低的 | 内部/外包 | 明显的 |
您学会了利用这些知识来设计针对每种类型的子域优化的解决方案。我们讨论了四种业务逻辑实现模式——事务脚本、活动记录、领域模型和事件源领域模型——以及每种模式发挥作用的场景。您还看到了三种架构模式,它们为业务逻辑的实现提供了所需的脚手架:分层架构、端口和适配器以及CQRS。图 E-1总结了使用这些模式进行战术决策的启发式方法。
You learned to leverage this knowledge to design solutions optimized for each type of subdomain. We discussed four business logic implementation patterns—transaction script, active record, domain model, and event sourced domain model—and the scenarios in which each pattern shines. You also saw three architectural patterns that provide the required scaffolding for the implementation of business logic: layered architecture, ports & adapters, and CQRS. Figure E-1 summarizes the heuristics for tactical decision-making using these patterns.
在第三部分,我们讨论了如何将理论转化为实践。您学习了如何通过促进 EventStorming 会话有效地构建通用语言,如何随着业务领域的发展保持设计的形状,以及如何在棕地项目中引入和开始使用领域驱动设计。
In Part III, we discussed how to turn theory into practice. You learned how to effectively build a ubiquitous language by facilitating an EventStorming session, how to keep the design in shape as the business domain evolves, and how to introduce and start using domain-driven design in brownfield projects.
在第 IV 部分中,我们讨论了领域驱动设计与其他方法和模式之间的相互作用:微服务、事件驱动架构和数据网格。我们看到 DDD 不仅可以与这些技术一起使用,而且它们实际上还可以相互补充。
In Part IV, we discussed the interplay between domain-driven design and other methodologies and patterns: microservices, event-driven architecture, and data mesh. We saw that not only can DDD be used in tandem with these techniques, but they in fact complement each other.
我希望这本书让你对领域驱动设计感兴趣。如果你想继续学习,这里有一些我衷心推荐的书。
I hope this book got you interested in domain-driven design. If you want to keep learning, here are some books that I wholeheartedly recommend.
Evans, E. (2003)。领域驱动设计:解决软件核心的复杂性。波士顿:Addison-Wesley。
埃里克·埃文斯 (Eric Evans) 的原创书籍,介绍了领域驱动设计方法。尽管它没有反映 DDD 的较新方面,例如领域事件和事件溯源,但它仍然是成为 DDD 黑带的必备读物。
Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Boston: Addison-Wesley.
Eric Evans’s original book that introduced the domain-driven design methodology. Although it doesn’t reflect newer aspects of DDD such as domain events and event sourcing, it’s still essential reading for becoming a DDD black belt.
Martraire, C. (2019)。动态文档:通过设计持续共享知识。波士顿:Addison-Wesley。
在本书中,Cyrille Martraire 提出了一种基于领域驱动设计的知识共享、文档和测试方法。
Martraire, C. (2019). Living Documentation: Continuous Knowledge Sharing by Design. Boston: Addison-Wesley.
In this book, Cyrille Martraire proposes a domain-driven design–based approach to knowledge sharing, documentation, and testing.
Vernon, V. (2013)。实施领域驱动设计。波士顿:Addison-Wesley。
另一个永恒的 DDD 经典。Vaughn Vernon 对领域驱动设计思维及其战略和战术工具集的使用进行了深入讨论和详细示例。作为学习基础,Vaughn 使用了 DDD 失败计划的真实示例,以及通过应用必要的课程更正使团队重新焕发活力的旅程。
Vernon, V. (2013). Implementing Domain-Driven Design. Boston: Addison-Wesley.
Another timeless DDD classic. Vaughn Vernon provides in-depth discussion and detailed examples of domain-driven design thinking and the use of its strategic and tactical toolset. As a learning foundation, Vaughn uses a real-world example of failing initiatives with DDD and the teams’ rejuvenated journey afforded by applying essential course corrections.
年轻的 G.(2017 年)。事件源系统中的版本控制。精益酒吧。
在第 7 章中,我们讨论了发展事件源系统可能具有挑战性。本书专门讨论这个主题。
Young, G. (2017). Versioning in an Event Sourced System. Leanpub.
In Chapter 7, we discussed that it can be challenging to evolve an event-sourced system. This book is dedicated to this topic.
Dehghani, Z.(预计于 2022 年出版)。数据网格:大规模交付数据驱动的价值。波士顿:奥莱利。
Zhamak Dehghani 是我们在第 16 章中讨论的数据网格模式的作者。在这本书中,Dehghani 解释了数据管理架构背后的原则,以及如何在实践中实施数据网格架构。
Dehghani, Z. (Expected to be published in 2022). Data Mesh: Delivering Data-Driven Value at Scale. Boston: O’Reilly.
Zhamak Dehghani is the author of the data mesh pattern that we discussed in Chapter 16. In this book, Dehghani explains the principles behind the data management architecture, as well as how to implement the data mesh architecture in practice.
福勒,M. (2002)。企业应用架构模式。波士顿:Addison-Wesley。
Fowler, M. (2002). Patterns of Enterprise Application Architecture. Boston: Addison-Wesley.
The classic application architecture patterns book that I quoted multiple times in Chapter 5 and Chapter 6. This is the book in which the transaction script, active record, and domain model patterns were originally defined.
Hohpe, G., & Woolf, B. (2003)。企业集成模式:设计、构建和部署消息传递解决方案。波士顿:Addison-Wesley。
第 9 章中讨论的许多模式最初都是在本书中介绍的。阅读本书了解更多组件集成模式。
Hohpe, G., & Woolf, B. (2003). Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Boston: Addison-Wesley.
Many of the patterns discussed in Chapter 9 were originally introduced in this book. Read this book for more component integration patterns.
理查森 C.(2019 年)。微服务模式:带有 Java 示例。纽约:曼宁出版社。
在本书中,Chris Richardson 提供了许多在构建基于微服务的解决方案时经常使用的模式的详细示例。讨论的模式包括我们在第 9 章中讨论的 saga、流程管理器和发件箱。
Richardson, C. (2019). Microservice Patterns: With Examples in Java. New York: Manning Publications.
In this book, Chris Richardson provides many detailed examples of patterns often used when architecting microservices-based solutions. Among the discussed patterns are saga, process manager, and outbox, which we discussed in Chapter 9.
Kaiser, S.(预计于 2022 年出版)。具有域驱动设计、Wardley 映射和团队拓扑的自适应系统。波士顿:Addison-Wesley。
Susanne Kaiser 分享了她通过利用领域驱动设计、Wardley 映射和团队拓扑对遗留系统进行现代化改造的经验。
Kaiser, S. (Expected to be published in 2022). Adaptive Systems with Domain-Driven Design, Wardley Mapping, and Team Topologies. Boston: Addison-Wesley.
Susanne Kaiser shares her experience of modernizing legacy systems by leveraging domain-driven design, Wardley mapping, and team topologies.
Tune, N.(预计 2022 年出版)。架构现代化:面向产品、领域和团队。精益酒吧。
在本书中,Nick Tune 深入讨论了如何利用领域驱动设计和其他技术来实现棕地项目架构的现代化。
Tune, N. (Expected to be published in 2022). Architecture Modernization: Product, Domain, & Team Oriented. Leanpub.
In this book, Nick Tune discusses in depth how to leverage domain-driven design and other techniques to modernize brownfield projects’ architecture.
Vernon, V. 和 Jaskula, T. (2021)。实施战略单体和微服务。波士顿:Addison-Wesley。
一本动手实践的书,作者在其中展示了永恒的软件工程工具,包括快速发现和学习、领域驱动的方法,以及处理正确实施基于单体和微服务的解决方案的复杂性,同时关注最重要的方面:提供创新的业务战略。
Vernon, V., & Jaskula, T. (2021). Implementing Strategic Monoliths and Microservices. Boston: Addison-Wesley.
A hands-on book in which the authors demonstrate ageless software engineering tools, including rapid discovery and learning, domain-driven approaches, and handling the intricacies of properly implementing monolith- and microservices-based solutions, all while focusing on the most important aspect: delivering innovative business strategy.
Vernon, V. 和 Jaskula, T. (2021)。战略单体和微服务。波士顿:Addison-Wesley。
在这本书中,Vaughn 和 Tomasz 通过探索如何使用基于发现的学习和领域驱动方法实现所有重要的创新,以及如何为工作选择最有目的的架构和工具来促进软件战略思维:微服务、单体、或混合,以及如何使它们协同工作。
Vernon, V., & Jaskula, T. (2021). Strategic Monoliths and Microservices. Boston: Addison-Wesley.
In this book Vaughn and Tomasz promote software strategic thinking by exploring how to achieve all-important innovations using discovery-based learning along with a domain-driven approach, and how to select the most purposeful architecture and tools for the job: microservices, monoliths, or a blend, and how to make them work together.
Brandolini, A.(尚未发表)。介绍 EventStorming。精益酒吧。
Alberto Brandolini 是 EventStorming 研讨会的创始人,在本书中,他详细解释了 EventStorming 背后的过程和基本原理。
Brandolini, A. (Not yet published). Introducing EventStorming. Leanpub.
Alberto Brandolini is the creator of the EventStorming workshop, and in this book, he explains in detail the process and rationale behind EventStorming.
Rayner, P.(尚未发表)。事件风暴手册。精益酒吧。
Rayner, P. (Not yet published). The EventStorming Handbook. Leanpub.
Paul Rayner explains how he uses EventStorming in practice, including numerous tips and tricks for facilitating a successful session.
就是这样!非常感谢您阅读本书。我希望你喜欢它并且你会使用你从中学到的东西。
That’s it! Thank you so much for reading this book. I hope you enjoyed it and that you will use what you’ve learned from it.
我希望你从本书中学到的是领域驱动设计工具背后的逻辑和原则。不要盲目地将领域驱动设计当作教条,而是要理解它所基于的推理。这种理解将显着增加您应用 DDD 并从中获得价值的机会。理解领域驱动设计的理念也是通过单独整合方法论的概念来利用价值的关键,尤其是在棕地项目中。
What I hope you take away from this book are the logic and the principles behind domain-driven design tools. Don’t follow domain-driven design blindly as a dogma, but rather understand the reasoning it is based on. This understanding will significantly increase your opportunities to apply DDD and gain value from it. Understanding the philosophy of domain-driven design is also the key to leveraging value by incorporating the methodology’s concepts individually, especially in brownfield projects.
最后,时刻关注你的无处不在的语言,当有疑问时,使用 EventStorming。祝你好运!
Finally, always watch your ubiquitous language, and when in doubt, do EventStorming. Good luck!
在本附录中,我将分享我的领域驱动设计之旅是如何开始的:一家初创公司的故事,在本示例中,我们将其称为“Marketnovus”。在 Marketnovus,我们从公司成立之日起就一直在采用 DDD 方法。多年来,我们不仅犯了所有可能的 DDD 错误,而且我们还有机会从这些错误中吸取教训并改正它们。我将使用这个故事和我们所犯的错误来展示 DDD 模式和实践在软件项目成功中所扮演的角色。
In this appendix, I will share how my domain-driven design journey started: the story of a start-up company that, for the purposes of this example, we’ll refer to as “Marketnovus.” At Marketnovus, we had been employing DDD methodology since the day the company was founded. Over the years, not only had we committed every possible DDD mistake, but we also had the opportunity to learn from those mistakes and fix them. I will use this story and the mistakes we made to demonstrate the role that DDD patterns and practices play in the success of a software project.
本案例研究由两部分组成。在第I 部分中,我将向您介绍 Marketnovus 的五个限界上下文的故事、做出的设计决策以及结果。在第二部分,我将讨论这些故事如何反映您在本书中学到的材料。
This case study consists of two parts. In the Part I, I’ll walk you through the stories of five of Marketnovus’s bounded contexts, what design decisions were made, and what the outcomes were. In the second part, I will discuss how these stories reflect the material you learned in this book.
在我们开始之前,我需要强调一下 Marketnovus 已经不存在了。因此,本附录绝不是宣传性的。此外,由于这是一家倒闭的公司,我可以坦诚地谈论我们的经历。
Before we begin, I need to stress that Marketnovus doesn’t exist anymore. As a result, this appendix is in no way promotional. Furthermore, since this is a defunct company, I’m free to speak honestly about our experiences.
在我们深入研究限界上下文及其设计方式之前,作为行为良好的 DDD 从业者,我们必须首先定义 Marketnovus 的业务领域。
Before we delve into the bounded contexts and how they were designed, as well-behaved DDD practitioners we have to start by defining Marketnovus’s business domain.
想象一下,您正在生产一种产品或一种服务。允许市场创新您将所有与营销相关的杂务外包出去。Marketnovus 的专家会为您的产品制定营销策略。它的撰稿人和平面设计师会制作大量创意材料,例如横幅和登陆页面,这些材料将用于开展宣传您产品的广告活动。这些活动产生的所有销售线索都将由 Marketnovus 的销售代理处理,他们会拨打电话并销售您的产品。图 A-1描述了这个过程。
Imagine you are producing a product or a service. Marketnovus allowed you to outsource all of your marketing-related chores. Marketnovus’s experts would come up with a marketing strategy for your product. Its copywriters and graphic designers would produce tons of creative material, such as banners and landing pages, that would be used to run advertising campaigns promoting your product. All the leads generated by these campaigns would be handled by Marketnovus’s sales agents, who would make the calls and sell your product. This process is depicted in Figure A-1.
最重要的是,这个营销过程提供了很多优化的机会,而这正是分析部门所负责的。他们分析了所有数据,以确保 Marketnovus 及其客户获得最大的收益,无论是通过精确定位最成功的活动、庆祝最有效的创意,还是确保销售代理正在寻找最有前途的线索。
Most importantly, this marketing process provided many opportunities for optimization, and that’s exactly what the analysis department was in charge of. They analyzed all the data to make sure Marketnovus and its clients were getting the biggest bang for their buck, whether by pinpointing the most successful campaigns, celebrating the most effective creatives, or ensuring that the sales agents were working on the most promising leads.
由于我们是一家自筹资金的公司,因此我们必须尽快启动。结果,在公司成立后,我们的软件系统的第一个版本必须实现我们价值链的前三分之一:
Since we were a self-funded company, we had to get rolling as fast as possible. As a result, right after the company was founded, the first version of our software system had to implement the first one-third of our value chain:
用于管理与外部发布者的合同和集成的系统
A system for managing contracts and integrations with external publishers
供我们的设计师管理创意材料的目录
A catalog for our designers to manage creative materials
运行广告活动的活动管理解决方案
A campaign management solution to run advertising campaigns
我不知所措,不得不找到一种方法来解决业务领域的所有复杂问题。幸运的是,在我们开始工作前不久,我读了一本承诺这一点的书。当然,我说的是埃里克·埃文斯 (Eric Evans) 的开创性著作,领域驱动设计:解决软件核心的复杂性问题。
I was overwhelmed and had to find a way to wrap my head around all the complexities of the business domain. Fortunately, not long before we started working, I read a book that promised just that. Of course, I’m talking about Eric Evans’s seminal work, Domain-Driven Design: Tackling Complexity at the Heart of Software.
如果您读过本书的前言,您就会知道 Evans 的书提供了我长期以来一直在寻找的答案:如何设计和实现业务逻辑。也就是说,对我来说,这不是一本初读时容易理解的书。尽管如此,我觉得仅仅通过阅读战术设计章节我就已经对 DDD 有了深刻的理解。
If you have read this book’s Preface, you know Evans’s book provided the answers I’d been seeking for quite a while: how to design and implement business logic. That said, for me it wasn’t an easy book to comprehend on the first read. Nevertheless, I felt like I’d already gotten a strong grasp of DDD just by reading the tactical design chapters.
猜猜系统最初是如何设计的?这肯定会让DDD 社区的某个杰出人士1感到非常自豪。
Guess how the system was initially designed? It would definitely make a certain prominent individual1 from the DDD community very proud.
我们的第一个解决方案的架构风格可以简洁地概括如同“无处不在”。代理商、活动、展示位置、漏斗、发布者:要求中的每一个名词都被称为集合。
The architectural style of our first solution could be neatly summarized as “aggregates everywhere.” Agency, campaign, placement, funnel, publisher: each and every noun in the requirements was proclaimed as an aggregate.
所有这些所谓的聚合都存在于一个巨大的、孤立的、有界的环境中。是的,一个巨大的、可怕的巨石,现在每个人都会警告你的那种。
All of those so-called aggregates resided in a huge, lone, bounded context. Yes, a big, scary monolith, the kind everyone warns you about nowadays.
当然,这些都不是总和。它们不提供任何事务边界,并且其中几乎没有任何行为。所有业务逻辑都在一个巨大的服务层中实现。
And of course, those were no aggregates. They didn’t provide any transactional boundaries, and they had almost no behavior in them. All the business logic was implemented in an enormous service layer.
当您打算实现域模型但最终使用活动记录模式时,它通常被称为“贫血领域模型”反模式。事后看来,这个设计是如何不实现领域模型的一个照搬书本的例子。然而,从商业角度来看,情况却大不相同。
When you aim to implement a domain model but end up with the active record pattern, it is often termed an “anemic domain model” antipattern. In hindsight, this design was a by-the-book example of how not to implement a domain model. However, things looked quite different from a business standpoint.
从商业的角度来看,这个项目算是一个巨大的成功!尽管架构存在缺陷,但我们能够在非常紧迫的上市时间内交付可工作的软件。我们是怎么做的?
From the business’s point of view, this project was considered a huge success! Despite the flawed architecture, we were able to deliver working software in a very aggressive time to market. How did we do it?
我们以某种方式设法提出了一种健壮的无处不在的语言。我们之前都没有任何在线营销经验,但我们仍然可以与领域专家进行对话。我们了解他们,他们也了解我们,令我们惊讶的是,领域专家竟然是非常好的人!他们真诚地感谢我们愿意向他们和他们的经验学习。
We somehow managed to come up with a robust ubiquitous language. None of us had any prior experience in online marketing, but we could still hold a conversation with domain experts. We understood them, they understood us, and to our astonishment, domain experts turned out to be very nice people! They genuinely appreciated the fact that we were willing to learn from them and their experience.
与领域专家的顺畅沟通让我们第一时间掌握了业务领域并实现了业务逻辑。是的,它是一个相当大的整体,但对于一个车库里的两个开发人员来说,它已经足够好了。同样,我们在非常紧迫的上市时间内生产了可用的软件。
The smooth communication with the domain experts allowed us to grasp the business domain in no time and implement its business logic. Yes, it was a pretty big monolith, but for two developers in a garage, it was just good enough. Again, we produced working software in a very aggressive time to market.
我们现阶段对领域驱动设计的理解可以用图 A-2中所示的简单图表来表示。
Our understanding of domain-driven design at this stage could be represented with the simple diagram shown in Figure A-2.
在我们部署活动管理解决方案后不久,线索开始涌入,我们很匆忙。我们的销售代理需要一个强大的客户关系管理 (CRM) 系统来管理销售线索及其生命周期。
Soon after we deployed the campaign management solution, leads started flowing in, and we were in a rush. Our sales agents needed a robust customer relationship management (CRM) system to manage the leads and their lifecycles.
CRM 必须汇总所有传入的线索,根据不同的参数对它们进行分组,并将它们分配到全球多个销售台。它还必须与我们客户的内部系统集成,以通知客户潜在客户生命周期的变化,并为我们的潜在客户提供更多信息。当然,CRM 必须提供尽可能多的优化机会。例如,我们需要能够确保代理商正在处理最有前途的线索,根据他们的资格和过去的表现将线索分配给代理商,并允许一个非常灵活的解决方案来计算代理商的佣金。
The CRM had to aggregate all incoming leads, group them based on different parameters, and distribute them across multiple sales desks around the globe. It also had to integrate with our clients’ internal systems, both to notify the clients about changes in the leads’ lifecycles and to complement our leads with additional information. And, of course, the CRM had to provide as many optimization opportunities as possible. For example, we needed to be able to make sure the agents were working on the most promising leads, assign leads to agents based on their qualifications and past performance, and allow a very flexible solution for calculating agents’ commissions.
由于没有现成的产品符合我们的要求,我们决定推出我们自己的 CRM 系统。
Since no off-the-shelf product fit our requirements, we decided to roll out our own CRM system.
最初的实施方法是继续关注战术模式。同样,我们将每个名词发音为一个集合,并将它们塞进同一个整体中。然而这一次,从一开始就感觉有些不对劲。
The initial implementation approach was to continue focusing on the tactical patterns. Again, we pronounced every noun as an aggregate and shoehorned them into the same monolith. This time, however, something felt wrong right from the start.
我们注意到,我们经常为这些“聚合”名称添加笨拙的前缀:例如,CRMLead 和 MarketingLead、MarketingCampaign 和 CRMCampaign。有趣的是,我们在与领域专家的对话中从未使用过这些前缀。不知何故,他们总能从上下文中理解意思。
We noticed that, all too often, we were adding awkward prefixes to those “aggregates” names: for example, CRMLead and MarketingLead, MarketingCampaign and CRMCampaign. Interestingly, we never used those prefixes in our conversations with the domain experts. Somehow, they always understood the meaning from the context.
然后我想起了领域驱动设计有一个我们迄今为止一直忽略的限界上下文的概念。重温 Evans 书中的相关章节后,我了解到限界上下文解决的问题与我们遇到的问题完全相同:它们保护无处不在的语言的一致性。此外,到那时,Vaughn Vernon 已经发表了他的“Effective Aggregate Design”论文。该论文明确指出了我们在设计聚合时所犯的所有错误。我们将聚合视为数据结构,但它们通过保护系统数据的一致性发挥更大的作用。
Then I recalled that domain-driven design has a notion of bounded contexts that we had been ignoring so far. After revisiting the relevant chapters of Evans’s book, I learned that bounded contexts solve exactly the same issue we were experiencing: they protect the consistency of the ubiquitous language. Furthermore, by that time, Vaughn Vernon had published his “Effective Aggregate Design” paper. The paper made explicit all the mistakes we were making when designing the aggregates. We were treating aggregates as data structures, but they play a much larger role by protecting the consistency of the system’s data.
我们退后一步,重新设计了 CRM 解决方案以反映这些启示。
We took a step back and redesigned the CRM solution to reflect these revelations.
我们首先将整体划分为两个不同的有界上下文:市场营销和 CRM。当然,我们并没有一路走到这里的微服务;我们只是做了最低限度的保护无处不在的语言。
We started by dividing our monolith into two distinct bounded contexts: marketing and CRM. Of course, we didn’t go all the way to microservices here; we just did the bare minimum to protect the ubiquitous language.
然而,在新的有界上下文 CRM 中,我们不会重复在营销系统中犯过的相同错误。没有更多的贫血领域模型!在这里,我们将实现一个真实的领域模型,其中包含真实的、按书本的聚合。特别是,我们发誓:
However, in the new bounded context, the CRM, we were not going to repeat the same mistakes we made in the marketing system. No more anemic domain models! Here we would implement a real domain model with real, by-the-book aggregates. In particular, we vowed that:
每个事务只会影响聚合的一个实例。
Each transaction would affect only one instance of an aggregate.
每个聚合本身将定义事务范围,而不是 ORM。
Instead of an ORM, each aggregate itself would define the transactional scope.
服务层将进行非常严格的节食,所有业务逻辑都将重构到相应的聚合中。
The service layer would go on a very strict diet, and all the business logic would be refactored into the corresponding aggregates.
我们非常热衷于以正确的方式做事。但是,很快,很明显,建模一个合适的领域模型是很困难的!
We were so enthusiastic about doing things the right way. But, soon enough, it became apparent that modeling a proper domain model is hard!
相对于营销系统,一切都花费了很多时间!几乎不可能在第一时间就确定交易边界。我们必须至少评估几个模型并对其进行测试,后来才发现我们没有考虑过的模型是正确的。以“正确”方式做事的代价非常高:很多时间。
Relative to the marketing system, everything took much more time! It was almost impossible to get the transactional boundaries right the first time. We had to evaluate at least a few models and test them, only to figure out later that the one we hadn’t thought about was the correct one. The price of doing things the “right” way was very high: lots of time.
很快,每个人都明白,我们绝对不可能在最后期限前完成!为了帮助我们解决问题,管理层决定将某些功能的实施工作卸载给……数据库管理员团队。
Soon it became obvious to everyone that there was no chance in hell we would meet the deadlines! To help us out, management decided to offload implementation of some of the features to…the database administrators team.
是的,在存储过程中实现业务逻辑。
Yes, to implement the business logic in stored procedures.
这一决定导致了很大的损失。不是因为 SQL 不是描述业务逻辑的最佳语言。不,真正的问题更加微妙和根本。
This one decision resulted in much damage down the line. Not because SQL is not the best language for describing business logic. No, the real issue was a bit more subtle and fundamental.
这种情况产生了一个隐含的有界上下文,其边界剖析了我们最复杂的业务实体之一:Lead。
This situation produced an implicit bounded context whose boundary dissected one of our most complex business entities: the Lead.
结果是两个团队在同一个业务组件上工作并实现密切相关的功能,但他们之间的交互最少。无处不在的语言?放过我吧!从字面上看,每个团队都有自己的词汇来描述业务领域及其规则。
The result was two teams working on the same business component and implementing closely related features, but with minimal interaction between them. Ubiquitous language? Give me a break! Literally, each team had its own vocabulary to describe the business domain and its rules.
模型不一致。没有共同的理解。知识被复制,同样的规则被执行了两次。请放心,当逻辑必须改变时,实现会立即不同步。
The models were inconsistent. There was no shared understanding. Knowledge was duplicated, and the same rules were implemented twice. Rest assured, when the logic had to change, the implementations went out of sync immediately.
不用说,该项目几乎没有按时交付,而且漏洞百出。多年来一直在雷达下飞行的令人讨厌的生产问题破坏了我们最宝贵的资产:我们的数据。
Needless to say, the project wasn’t delivered anywhere near on time, and it was full of bugs. Nasty production issues that had flown under the radar for years corrupted our most precious asset: our data.
摆脱这种混乱局面的唯一方法是完全重写 Lead 聚合,这次使用适当的边界,几年后我们就这样做了。这并不容易,但混乱太严重了,没有其他办法解决。
The only way out of this mess was to completely rewrite the Lead aggregate, this time with proper boundaries, which we did a couple of years later. It wasn’t easy, but the mess was so bad there was no other way around it.
尽管这个项目在商业标准上非常惨败,但我们对领域驱动设计的理解有所发展:构建一种无处不在的语言,使用限界上下文保护其完整性,而不是在任何地方实施贫血领域模型,而是在任何地方实施适当的领域模型. 该模型如图 A-3所示。
Even though this project failed pretty miserably by business standards, our understanding of domain-driven design evolved a bit: build a ubiquitous language, protect its integrity using bounded contexts, and instead of implementing an anemic domain model everywhere, implement a proper domain model everywhere. This model is shown in Figure A-3.
当然,这里缺少域驱动设计的一个关键部分:子域、它们的类型以及它们如何影响系统的设计。
Of course, a crucial part of domain-driven design was missing here: subdomains, their types, and how they affect a system’s design.
最初我们想尽可能做到最好,但我们最终浪费了时间和精力来构建支持子域的域模型。正如 Eric Evans 所说,并不是大型系统的所有部分都会设计得很好。我们通过艰难的方式学到了这一点,我们想在我们的下一个项目中使用获得的知识。
Initially we wanted to do the best job possible, but we ended up wasting time and effort on building domain models for supporting subdomains. As Eric Evans put it, not all of a large system will be well designed. We learned that the hard way, and we wanted to use the acquired knowledge in our next project.
CRM 系统推出后,我们怀疑隐含的子域分布在市场营销和 CRM 中。每当必须修改处理传入客户事件的过程时,我们都必须在营销和 CRM 限界上下文中引入更改。
After the CRM system was rolled out, we suspected that an implicit subdomain was spread across marketing and CRM. Whenever the process of handling incoming customer events had to be modified, we had to introduce changes both in the marketing and CRM bounded contexts.
由于从概念上讲这个过程不属于它们中的任何一个,我们决定将这个逻辑提取到一个称为“事件处理程序”的专用限界上下文中,如图 A-4所示。
Since conceptually this process didn’t belong to any of them, we decided to extract this logic into a dedicated bounded context called “event crunchers,” shown in Figure A-4.
由于我们没有从四处移动数据的方式中赚到任何钱,并且没有任何可以使用的现成解决方案,因此事件处理器类似于一个支持子域。我们是这样设计的。
Since we didn’t make any money out of the way we move data around, and there weren’t any off-the-shelf solutions that could have been used, event crunchers resembled a supporting subdomain. We designed it as such.
这次没什么特别的:只是分层架构和一些简单的交易脚本。这个解决方案效果很好,但只持续了一段时间。
Nothing fancy this time: just layered architecture and some simple transaction scripts. This solution worked great, but only for a while.
随着我们业务的发展,我们在事件处理器中实现了越来越多的功能。它始于商业智能 (BI) 人员要求一些标志:一个标志用于标记新联系人,另一个标志用于标记各种首次事件,更多标志用于指示某些业务不变量,等等。
As our business evolved, we implemented more and more features in the event crunchers. It started with business intelligence (BI) people asking for some flags: a flag to mark a new contact, another one to mark various first-time events, some more flags to indicate some business invariants, and so on.
最终,这些简单的标志演变成具有复杂规则和不变量的真实业务逻辑。最初作为交易脚本发展成为成熟的核心业务子域。
Eventually, those simple flags evolved into a real business logic, with complex rules and invariants. What started out as transaction scripts evolved into a full-fledged core business subdomain.
不幸的是,当您将复杂的业务逻辑实现为事务脚本时,并没有什么好结果。由于我们没有调整我们的设计来应对复杂的业务逻辑,我们最终得到了一个非常大的泥球。对代码库的每次修改都变得越来越昂贵,质量下降,我们被迫重新考虑事件处理器的设计。一年后我们做到了。到那时,业务逻辑已经变得非常复杂,只能通过事件溯源来解决。我们将事件处理程序的逻辑重构为事件源域模型,其他限界上下文订阅其事件。
Unfortunately, nothing good happens when you implement complex business logic as transaction scripts. Since we didn’t adapt our design to cope with the complex business logic, we ended up with a very big ball of mud. Each modification to the codebase became more and more expensive, quality went downhill, and we were forced to rethink the event crunchers design. We did that a year later. By that time, the business logic had become so complex that it could only be tackled with event sourcing. We refactored the event crunchers’ logic into an event-sourced domain model, with other bounded contexts subscribing to its events.
有一天,销售台经理要求我们自动化一个简单的他们一直在手动执行繁琐的程序:计算销售代理的佣金。
One day, the sales desk managers asked us to automate a simple yet tedious procedure they had been doing manually: calculate the commissions for the sales agents.
同样,它开始时很简单:每月一次,只需计算每个代理商销售额的百分比并将报告发送给经理。和以前一样,我们考虑这是否是一个核心子域。答案是否定的。我们没有发明任何新东西,也没有从这个过程中赚钱,如果可以购买现有的实现,我们肯定会购买。不是核心,不是通用的,而是另一个支持子域。
Again, it started out simple: once a month, just calculate a percentage of each agent’s sales and send the report to the managers. As before, we contemplated whether this was a core subdomain. The answer was no. We weren’t inventing anything new, weren’t making money out of this process, and if it was possible to buy an existing implementation, we definitely would. Not core, not generic, but another supporting subdomain.
我们相应地设计了解决方案:活动记录对象,由“智能”服务层编排,如图A-5所示。
We designed the solution accordingly: active record objects, orchestrated by a “smart” service layer, as shown in Figure A-5.
一旦这个过程变得自动化,男孩,公司里的每个人都变得有创意了吗?我们的分析师希望优化这个过程。他们想尝试不同的百分比,将百分比与销售额和价格挂钩,解锁额外的佣金以实现不同的目标,等等。猜猜最初的设计是什么时候崩溃的?
Once the process became automated, boy, did everyone in the company become creative about it. Our analysts wanted to optimize the heck out of this process. They wanted to try out different percentages, tie percentages to sales amounts and prices, unlock additional commissions for achieving different goals, and on and on. Guess when the initial design broke down?
再一次,代码库开始变成一个无法管理的泥球。添加新功能变得越来越昂贵,错误开始出现——当你在处理金钱时,即使是最小的错误也会产生巨大的后果。
Again, the codebase started turning into an unmanageable ball of mud. Adding new features became more and more expensive, bugs started to appear—and when you’re dealing with money, even the smallest bug can have BIG consequences.
与事件处理程序项目一样,在某些时候我们再也无法忍受了。我们不得不丢弃旧代码并从头开始重写解决方案,这次是作为事件源域模型。
As with the event crunchers project, at some point we couldn’t bear it anymore. We had to throw away the old code and rewrite the solution from the ground up, this time as an event-sourced domain model.
就像在 event crunchers 项目中一样,业务领域最初被归类为支持领域。随着系统的发展,它逐渐变异为一个核心子域:我们找到了从这些过程中赚钱的方法。但是,这两个有界上下文之间存在显着差异。
And just as in the event crunchers project, the business domain was initially categorized as a supporting one. As the system evolved, it gradually mutated into a core subdomain: we found ways to make money out of these processes. However, there is a striking difference between these two bounded contexts.
对于奖金项目,我们有一种无处不在的语言。虽然最初的实现是基于活动记录的,我们仍然可以拥有一种无处不在的语言。
For the bonuses project, we had a ubiquitous language. Even though the initial implementation was based on active records, we could still have a ubiquitous language.
随着领域的复杂性增加,领域专家使用的语言也变得越来越复杂。在某些时候,它不能再使用活动记录建模!这种认识使我们能够比在事件处理程序项目中更早地注意到设计更改的必要性。多亏了无处不在的语言,我们没有试图将方钉插入圆孔,从而节省了大量时间和精力。
As the domain’s complexity grew, the language used by the domain experts got more and more complicated as well. At some point, it could no longer be modeled using active records! This realization allowed us to notice the need for a change in the design much earlier than we did in the event crunchers project. We saved a lot of time and effort by not trying to fit a square peg into a round hole, thanks to the ubiquitous language.
至此,我们对领域驱动设计的理解终于演变成经典:无处不在的语言、限界上下文、不同类型的子域,各取所需,如图A-6所示。
At this point, our understanding of domain-driven design had finally evolved into a classic one: ubiquitous language, bounded contexts, and different types of subdomains, each designed according to its needs, as shown in Figure A-6.
然而,对于我们的下一个项目来说,事情发生了意想不到的转变。
However, things took quite an unexpected turn for our next project.
我们的管理层正在寻找一个有利可图的新垂直领域。他们决定尝试利用我们的能力来产生大量的潜在客户并将其出售给我们以前没有合作过的小客户。这个项目被称为“营销中心”。
Our management was looking for a profitable new vertical. They decided to try using our ability to generate a massive number of leads and sell them to smaller clients, ones we hadn’t worked with before. This project was called “marketing hub.”
由于管理层已将此业务领域定义为新的盈利机会,因此它显然是一个核心业务领域。因此,在设计方面,我们拿出了重炮:事件源域模型和 CQRS。此外,当时,一个新的流行词微服务开始获得广泛关注。我们决定试一试。
Since management had defined this business domain as a new profit opportunity, it was clearly a core business domain. Hence, designwise, we pulled out the heavy artillery: event-sourced domain model and CQRS. Also, back then, a new buzzword, microservices, started gaining lots of traction. We decided to give it a try.
我们的解决方案类似于图 A-7中所示的实现。
Our solution looked like the implementation shown in Figure A-7.
小型服务,每个服务都有自己的数据库,它们之间同时进行同步和异步通信:在纸面上,它看起来像是一个完美的解决方案设计。实际上,并没有那么多。
Small services, each having its own database, with both synchronous and asynchronous communication between them: on paper, it looked like a perfect solution design. In practice, not so much.
我们天真地接触微服务,认为服务越小越好。因此,我们围绕聚合绘制了服务边界。在 DDD 术语中,每个聚合都变成了自己的有界上下文。
We näively approached microservices thinking that the smaller the service was, the better. So we drew service boundaries around the aggregates. In DDD lingo, each aggregate became a bounded context on its own.
同样,最初这个设计看起来很棒。它使我们能够根据其特定需求实施每项服务。只有一个会使用事件溯源,其余的将是基于状态的聚合。而且,它们都可以独立维护和发展。
Again, initially this design looked great. It allowed us to implement each service according to its specific needs. Only one would be using event sourcing, and the rest would be state-based aggregates. Moreover, all of them could be maintained and evolved independently.
然而,随着系统的发展,这些服务变得越来越繁琐。最终,几乎每个服务都需要来自所有其他服务的数据来完成其某些操作。结果?原本打算成为一个解耦系统的东西最终变成了一个分布式单体:维护起来绝对是一场噩梦。
However, as the system grew, those services became more and more chatty. Eventually, almost each service required data from all the other services to complete some of its operations. The result? What was intended to be a decoupled system ended up being a distributed monolith: an absolute nightmare to maintain.
不幸的是,这个架构还有另一个更根本的问题。为了实施营销中心,我们使用了最复杂的业务领域建模模式:领域模型和事件源领域模型。我们精心设计了这些服务。但这一切都是徒劳的。
Unfortunately, there was another, much more fundamental issue we had with this architecture. To implement the marketing hub, we used the most complex patterns for modeling the business domain: domain model and event-sourced domain model. We carefully crafted those services. But it all was in vain.
尽管该企业将营销中心视为核心子域,但它没有技术复杂性。在这个复杂的架构背后是一个非常简单的业务逻辑,一个简单到可以使用普通活动记录来实现的业务逻辑。
Despite the fact that the business considered the marketing hub to be a core subdomain, it had no technical complexity. Behind that complex architecture stood a very simple business logic, one so simple that it could have been implemented using plain active records.
事实证明,这些商人希望通过利用我们与其他公司的现有关系而不是通过使用聪明的算法来获利。
As it turned out, the businesspeople were looking to profit by leveraging our existing relationships with other companies, and not through the use of clever algorithms.
技术复杂性最终远高于业务复杂性。为了描述这种复杂性的差异,我们使用术语accidental complexity,我们的初始设计最终就是这样。该系统设计过度。
The technical complexity ended up being much higher than the business complexity. To describe such discrepancies in complexities, we use the term accidental complexity, and our initial design ended up being exactly that. The system was overengineered.
这些是我想告诉您的五个限界上下文:市场营销、CRM、事件处理程序、奖金和营销中心。当然,像 Marketnovus 这样广泛的业务领域需要更多的限界上下文,但我想分享我们从中学到最多的限界上下文。
Those were the five bounded contexts I wanted to tell you about: marketing, CRM, event crunchers, bonuses, and marketing hub. Of course, such a wide business domain as Marketnovus entailed many more bounded contexts, but I wanted to share the bounded contexts we learned from the most.
现在我们已经了解了五个限界上下文,让我们从不同的角度来看这个问题。领域驱动设计的核心元素的应用或误用如何影响我们的结果?让我们来看看。
Now that we’ve walked through the five bounded contexts, let’s look at this from a different perspective. How did application or misapplication of core elements of domain-driven design influence our outcomes? Let’s take a look.
以我的经验,无处不在的语言是“核心子域”领域驱动设计。与我们的领域专家说同一种语言的能力对我们来说是不可或缺的。事实证明,这是一种比测试或文档更有效的知识共享方式。
In my experience, ubiquitous language is the “core subdomain” of domain-driven design. The ability to speak the same language with our domain experts has been indispensable to us. It turned out to be a much more effective way to share knowledge than tests or documents.
此外,无处不在的语言的存在一直是我们项目成功的主要预测因素:
Moreover, the presence of a ubiquitous language has been a major predictor of a project’s success for us:
刚开始的时候,我们对营销系统的实施还很不完善。然而,强大的通用语言弥补了架构上的缺陷,使我们能够实现项目的目标。
When we started, our implementation of the marketing system was far from perfect. However, the robust ubiquitous language compensated for the architectural shortcomings and allowed us to deliver the project’s goals.
在 CRM 环境中,我们搞砸了。无意中,我们有两种语言描述相同的业务领域。我们努力进行适当的设计,但由于沟通问题,我们最终搞得一团糟。
In the CRM context, we screwed it up. Unintentionally, we had two languages describing the same business domain. We strived to have a proper design, but because of the communication issues we ended up with a huge mess.
event crunchers 项目最初是一个简单的支持子域,我们没有投资于无处不在的语言。当复杂性开始增加时,我们很后悔这个决定。如果我们最初从一种无处不在的语言开始,那么我们花费的时间会少得多。
The event crunchers project started as a simple supporting subdomain, and we didn’t invest in the ubiquitous language. We regretted this decision big time when the complexity started growing. It would have taken us much less time if we initially started with a ubiquitous language.
在奖金项目中,业务逻辑变得复杂了几个数量级,但是无处不在的语言让我们更早地注意到需要改变实施策略。
In the bonuses project, the business logic became more complex by orders of magnitude, but the ubiquitous language allowed us to notice the need for a change in the implementation strategy much earlier.
因此,无论您是在核心、支持还是通用子域上工作,通用语言都不是可有可无的。
Hence, ubiquitous language is not optional, regardless of whether you’re working on a core, supporting, or generic subdomain.
我们了解到尽早投资于无处不在的语言的重要性。如果一种语言在公司中已经使用了一段时间(就像我们的 CRM 系统一样),那么“修复”一种语言需要付出巨大的努力和耐心。我们能够修复实施。这并不容易,但最终我们做到了。然而,对于语言而言,情况并非如此。多年来,一些人仍在使用最初实施中定义的相互矛盾的术语。
We learned the importance of investing in the ubiquitous language as early as possible. It requires immense effort and patience to “fix” a language if it has been spoken for a while in a company (as was the case with our CRM system). We were able to fix the implementation. It wasn’t easy, but eventually we did it. That’s not the case, however, for the language. For years, some people were still using the conflicting terms defined in the initial implementation.
正如您在第 1 章中了解到的,存在三种类型的子域——核心、支持和通用——在设计解决方案时识别起作用的子域很重要。
As you learned in Chapter 1, there are three types of subdomains— core, supporting, and generic—and it’s important to identify the subdomains at play when designing the solution.
识别子域的类型可能具有挑战性。正如我们在第 1 章中讨论的那样,在与您正在构建的软件系统相关的粒度级别上识别子域非常重要。例如,我们的营销中心计划旨在成为公司的额外利润来源。然而,此功能的软件方面是一个支持子域,而利用与其他公司的关系和合同才是真正的竞争优势,真正的核心子域。
It can be challenging to identify a subdomain’s type. As we discussed in Chapter 1, it’s important to identify the subdomains at the granularity level that is relevant to the software system you are building. For example, our marketing hub initiative was intended to be the company’s additional profit source. However, the software aspect of this functionality was a supporting subdomain, while leveraging the relationships and contracts with other companies was the actual competitive advantage, the real core subdomain.
此外,正如您在第 11 章中了解到的,仅识别子域的类型是不够的。您还必须了解子域可能演变为另一种类型。在 Marketnovus,我们见证了子域类型变化的几乎所有可能组合:
Furthermore, as you learned in Chapter 11, it’s not enough to identify a subdomain’s type. You also have to be aware of the possible evolutions of the subdomain into another type. At Marketnovus, we witnessed almost all the possible combinations of changes in subdomain types:
事件处理程序和奖金最初都是作为支持子域,但一旦我们发现了将这些过程货币化的方法,它们就成为了我们的核心子域。
Both the event crunchers and bonuses started as supporting subdomains, but once we discovered ways to monetize these processes, they became our core subdomains.
在营销方面,我们实施了自己的创意目录。它并没有什么特别或复杂的地方。然而,几年后,出现了一个开源项目,它提供了比我们最初拥有的更多的功能。一旦我们用这个产品替换了我们的实现,支持的子域就变成了通用子域。
In the marketing context, we implemented our own creative catalog. There was nothing really special or complex about it. However, a few years later, an open source project came out that offered even more features than we originally had. Once we replaced our implementation with this product, the supporting subdomain became a generic one.
在 CRM 环境中,我们有一种算法可以识别最有希望的潜在客户。随着时间的推移,我们对其进行了改进并尝试了不同的实现,但最终它被运行在云供应商的托管服务中的机器学习模型所取代。从技术上讲,核心子域变得通用。
In the CRM context, we had an algorithm that identified the most promising leads. We refined it over time and tried different implementations, but eventually it was replaced with a machine learning model running in a cloud vendor’s managed service. Technically, a core subdomain became generic.
正如我们所见,我们的营销中心系统最初是一个核心,但最终成为一个支持性的子域,因为竞争优势存在于一个完全不同的维度。
As we’ve seen, our marketing hub system started as a core, but ended up being a supporting subdomain, since the competitive edge resided in a completely different dimension.
正如您在本书中了解到的那样,子域类型会影响范围广泛的设计决策。未能正确识别子域可能是一个代价高昂的错误,例如,在事件处理器和营销中心的情况下。
As you’ve learned throughout this book, the subdomain types affect a wide range of design decisions. Failing to properly identify a subdomain can be a costly mistake as, for example, in the case of the event crunchers and the marketing hub.
这是我在 Marketnovus 想出的一个技巧来防止子域的识别:颠倒子域和战术设计决策之间的关系。选择业务逻辑实现模式。没有投机或镀金;只需选择适合手头要求的模式。接下来,将所选模式映射到合适的子域类型。最后,用业务愿景验证识别出的子域类型。
Here is a trick I came up with at Marketnovus to foolproof the identification of subdomains: reverse the relationship between subdomains and tactical design decisions. Choose the business logic implementation pattern. No speculation or gold plating; simply choose the pattern that fits the requirements at hand. Next, map the chosen pattern to a suitable subdomain type. Finally, verify the identified subdomain type with the business vision.
颠倒子域和战术设计决策之间的关系可以在您和企业之间建立额外的对话。有时,商人需要我们,就像我们需要他们一样。
Reversing the relationship between subdomains and tactical design decisions creates an additional dialogue between you and the business. Sometimes businesspeople need us as much as we need them.
如果他们认为某项业务是核心业务,但您可以在一天内破解它,那么这表明您需要寻找更细粒度的子域,或者应该对该业务的可行性提出质疑。
If they think something is a core business, but you can hack it in a day, then it is either a sign that you need to look for finer-grained subdomains or that questions should be raised about the viability of that business.
另一方面,如果一个子域被业务视为支持子域但只能使用高级建模技术实现,事情就会变得有趣:域模型或事件源域模型。
On the other hand, things get interesting if a subdomain is considered a supporting one by the business but can only be implemented using the advanced modeling techniques: domain model or event-sourced domain model.
首先,业务人员可能对他们的要求过于有创意,最终导致意外的业务复杂性。它发生了。在这种情况下,可以而且可能应该简化要求。
First, the businesspeople may have gotten overly creative with their requirements and ended up with accidental business complexity. It happens. In such a case, the requirements can, and probably should, be simplified.
其次,可能是商人还没有意识到他们使用这个子域来获得额外的竞争优势。这发生在奖金项目的案例中。通过发现这种不匹配,您可以帮助企业更快地确定新的利润来源。
Second, it might be that the businesspeople don’t yet realize they employ this subdomain to gain an additional competitive edge. This happened in the case of the bonuses project. By uncovering this mismatch, you’re helping the business identify new profit sources faster.
最重要的是,在实施系统的业务逻辑时永远不要忽视“痛苦”。这是发展和改进业务领域模型或战术设计决策的关键信号。在后一种情况下,这意味着子域已经进化,是时候回过头来重新考虑它的类型和实现策略了。如果类型发生了变化,请与领域专家交谈以了解业务环境。如果您需要重新设计实现以满足新的业务现实,请不要害怕这种变化。一旦有意识地决定如何对业务逻辑建模并且您了解所有可能的选项,就可以更容易地对此类更改做出反应并将实现重构为更精细的模式。
Most importantly, never ignore “pain” when implementing the system’s business logic. It is a crucial signal to evolve and improve either the model of the business domain or the tactical design decisions. In the latter case, it means the subdomain has evolved, and it’s time to go back and rethink its type and implementation strategy. If the type has changed, talk with the domain experts to understand the business context. If you need to redesign the implementation to meet new business realities, don’t be afraid of this kind of change. Once the decision of how to model the business logic is made consciously and you’re aware of all the possible options, it becomes much easier to react to such a change and refactor the implementation to a more elaborate pattern.
在 Marketnovus,我们尝试了很多策略来设置限界上下文的边界:
At Marketnovus, we tried quite a few strategies for setting the boundaries of bounded contexts:
语言边界:我们将最初的整体拆分为营销和 CRM 环境,以保护它们无处不在的语言。
Linguistic boundaries: We split our initial monolith into marketing and CRM contexts to protect their ubiquitous languages.
基于子域的边界:我们的许多子域都是在它们自己的有界上下文中实现的;例如,事件计算器和奖金。
Subdomain-based boundaries: Many of our subdomains were implemented in their own bounded contexts; for example, event crunchers and bonuses.
基于实体的边界:正如我们之前所讨论的,这种方法在营销中心项目中取得的成功有限,但在其他项目中却奏效了。
Entity-based boundaries: As we discussed earlier, this approach had limited success in the marketing hub project, but it worked in others.
自杀边界:您可能还记得,在 CRM 的初始实施中,我们将聚合分解为两个不同的有界上下文。永远不要在家里尝试这个,好吗?
Suicidal boundaries: As you may remember, in the initial implementation of the CRM we dissected an aggregate into two different bounded contexts. Never try this at home, okay?
推荐使用这些策略中的哪一个?它们都不适合所有情况。根据我们的经验,从较大的服务中提取服务比从太小的服务开始要安全得多。因此,我们更愿意从更大的边界开始,然后随着对业务的了解越来越多,再分解它们。这些初始边界有多宽?正如我们在第 11 章中讨论的那样,这一切都可以追溯到业务领域:您对业务领域了解的越少,初始边界就越宽。
Which of these strategies is the recommended one? None of them fits in all cases. In our experience, it was much safer to extract a service out of a bigger one than to start with services that are too small. Hence, we preferred to start with bigger boundaries and decompose them later, as more knowledge was acquired about the business. How wide are those initial boundaries? As we discussed in Chapter 11, it all goes back to the business domain: the less you know about the business domain, the wider the initial boundaries.
这种启发式方法对我们很有帮助。例如,在市场营销和 CRM 限界上下文的情况下,每个上下文都包含多个子域。随着时间的推移,我们逐渐将最初宽泛的边界分解为微服务。正如我们在第 14 章中定义的那样,在限界上下文的整个演化过程中,我们都停留在安全边界的范围内。只有在获得足够的业务领域知识后,我们才能通过重构来避免越过安全边界。
This heuristic served us well. For example, in the cases of the marketing and CRM bounded contexts, each encompassed multiple subdomains. As time passed, we gradually decomposed the initially wide boundaries into microservices. As we defined in Chapter 14, throughout the evolution of the bounded contexts, we stayed in the range of the safe boundaries. We were able to avoid going past the safe boundaries by doing the refactoring only after gaining enough knowledge of the business domain.
在 Marketnovus 的限界上下文的故事中,我展示了我们对领域驱动设计的理解是如何随着时间的推移而演变的(请参阅图 A-6进行复习):
In the stories of Marketnovus’s bounded contexts I showed how our understanding of domain-driven design evolved through time (refer to Figure A-6 for a refresher):
我们总是从与领域专家一起构建一种无处不在的语言开始,以尽可能多地了解业务领域。
We always started by building a ubiquitous language with the domain experts to learn as much as possible about the business domain.
在模型冲突的情况下,我们按照无处不在的语言的语言边界将解决方案分解为有界上下文。
In the case of conflicting models, we decomposed the solution into bounded contexts, following the linguistic boundaries of the ubiquitous language.
我们在每个有界上下文中确定了子域的边界及其类型。
We identified the subdomains’ boundaries and their types in each bounded context.
对于每个子域,我们通过使用战术设计启发式来选择实施策略。
For each subdomain we chose an implementation strategy by using tactical design heuristics.
我们用战术设计产生的子域类型验证了初始子域类型。在类型不匹配的情况下,我们与业务进行了讨论。有时这种对话会导致需求发生变化,因为我们能够为产品所有者提供有关项目的新视角。
We verified the initial subdomain types with those resulting from the tactical design. In cases of mismatching types, we discussed them with the business. Sometimes this dialogue led to changes in the requirements, because we were able to provide a new perspective on the project to the product owners.
随着获得更多的领域知识,如果需要,我们将有界上下文进一步分解为边界更窄的上下文。
As more domain knowledge was acquired, and if it was needed, we decomposed the bounded contexts further into contexts with narrower boundaries.
如果我们将这种领域驱动设计的愿景与我们开始时的愿景进行比较,我会说主要区别在于我们从“无处不在的聚合”变成了“无处不在的语言”。
If we compare this vision of domain-driven design with the one we started with, I’d say the main difference is that we went from “aggregates everywhere” to “ubiquitous language everywhere.”
临别之际,既然我已经告诉你 Marketnovus 是如何开始的,我想分享一下它是如何结束的。
In parting, since I’ve told you the story of how Marketnovus started, I want to share how it ended.
公司很快盈利,最终被最大的客户收购。当然,我不能将它的成功仅仅归功于领域驱动设计。然而,在那些年里,我们一直处于“启动模式”。
The company became profitable very quickly, and eventually it was acquired by its biggest client. Of course, I cannot attribute its success solely to domain-driven design. However, during all those years, we were constantly in “start-up mode.”
我们在以色列所说的“创业模式”在世界其他地方被称为“混乱”:不断变化的业务需求和优先事项、紧迫的时间框架以及小型研发团队。DDD 使我们能够解决所有这些复杂性并继续交付可工作的软件。因此,当我回头看时,我们在领域驱动设计上的赌注得到了充分的回报。
What we term “start-up mode” in Israel is called “chaos” in the rest of the world: constantly changing business requirements and priorities, aggressive time frames, and a tiny R&D team. DDD allowed us to tackle all of these complexities and keep delivering working software. Hence, when I look back, the bet we placed on domain-driven design paid off in full.
D: B and C. Only core subdomains provide competitive advantages that differentiate the company from other players in its industry.
B:通用。通用子域很复杂,但不会带来任何竞争优势。因此,最好使用经过实战验证的现有解决方案。
B: Generic. Generic subdomains are complex but do not entail any competitive advantage. Hence, it’s preferable to use an existing, battle-proven solution.
核心。核心子域预计将是最不稳定的,因为这些是公司旨在提供新解决方案的领域,并且通常需要相当多的交互才能找到最优化的解决方案。
A: Core. Core subdomains are expected to be the most volatile since these are areas in which the company aims to provide new solutions and it often requires quite a few interactions to find the most optimized solution.
WolfDesk 的业务领域是帮助台管理系统。
WolfDesk’s business domain is Help Desk management systems.
我们可以确定以下核心子域,使 WolfDesk 能够从竞争对手中脱颖而出并支持其商业模式:
票证生命周期管理算法,旨在关闭票证,从而鼓励用户打开新票证
防止滥用其商业模式的欺诈检测系统
支持自动驾驶仪,既可以减轻租户支持代理的工作,又可以进一步缩短工单的使用寿命
We can identify the following core subdomains that allow WolfDesk to differentiate itself from its competitors and support its business model:
Ticket lifecycle management algorithm that is intended to close tickets and thus encourage users to open new ones
Fraud detection system to prevent abuse of its business model
Support autopilot that both eases the tenants’ support agents’ work and further reduces the tickets’ lifespan
在公司的描述中可以识别以下支持子域:
管理租户的门票类别
管理租户的产品,客户可以就这些产品开具支持票
输入租户支持代理的工作时间表
The following supporting subdomains can be identified in the description of the company:
Management of a tenant’s ticket categories
Management of a tenant’s products, regarding which the customers can open support tickets
Entry of a tenant’s support agents’ work schedules
在公司的描述中可以识别以下通用子域:
验证和授权用户的“行业标准”方式
使用外部提供商进行身份验证和授权 (SSO)
公司利用无服务器计算基础设施来确保弹性可扩展性并最大限度地降低新租户入职的计算成本
The following generic subdomains can be identified in the description of the company:
“Industry standard” ways of authenticating and authorizing users
Using external providers for authentication and authorization (SSO)
The serverless compute infrastructure the company leverages to ensure elastic scalability and minimize the compute costs of onboarding new tenants
D:项目的所有利益相关者都应该贡献他们对业务领域的知识和理解。
D: All of the project’s stakeholders should contribute their knowledge and understanding of the business domain.
D:所有与项目相关的沟通都应该使用通用语言。软件的源代码也应该“说”它的通用语言。
D: A ubiquitous language should be used in all project-related communication. The software’s source code should also “speak” its ubiquitous language.
WolfDesk 的客户是租户。要开始使用该系统,租户需要完成一个快速的入职流程。该公司的收费模式是根据在一个收费期内打开的工单数量。工单生命周期管理算法可确保自动关闭非活动工单。WolfDesk 的欺诈检测算法可防止租户滥用其商业模式。支持自动驾驶功能会尝试自动为新工单寻找解决方案。工单属于支持类别并与产品相关联租户为其提供支持。支持代理只能在他们的工作时间内处理工单,这由他们的班次安排定义。
WolfDesk’s customers are tenants. To start using the system, tenants go through a quick onboarding process. The company’s charging model is based on the number of tickets that were opened during a charging period. The ticket lifecycle management algorithm ensures that inactive tickets are automatically closed. WolfDesk’s fraud detection algorithm prevents tenants from abusing its business model. The support autopilot functionality tries to find solutions for new tickets automatically. A ticket belongs to a support category and is associated with a product for which the tenant provides support. A support agent can only process tickets during their work time, which is defined by their shift schedules.
B:设计了限界上下文,同时发现了子域。
B: Bounded contexts are designed, while subdomains are discovered.
D:以上都是。有界上下文是模型的边界,模型仅适用于其有界上下文。限界上下文在独立的项目/解决方案中实现,因此允许每个限界上下文都有自己的开发生命周期。最后,有界上下文应该由一个开发团队来实现,因此,它也是一个所有权边界。
D: All of the above. A bounded context is a boundary of a model, and a model is only applicable in its bounded context. Bounded contexts are implemented in independent projects/solutions, thus allowing each bounded context to have its own development lifecycle. Finally, a bounded context should be implemented by a single development team, and therefore, it is also an ownership boundary.
D:这取决于。对于所有项目和案例,都没有完美大小的限界上下文。模型、组织约束和非功能性需求等不同因素会影响限界上下文的最佳范围。
D: It depends. There is no perfect size of a bounded context for all projects and cases. Different factors, such as models, organizational constraints, and nonfunctional requirements, affect the optimum scope of a bounded context.
D:B、C 正确。限界上下文应该只属于一个团队。同时,同一个团队可以拥有多个限界上下文。
D: B and C are correct. A bounded context should be owned by one team only. At the same time, the same team can own multiple bounded contexts.
可以肯定的是,实现票证生命周期的操作模型将不同于用于欺诈检测和支持自动驾驶功能的模型。欺诈检测算法通常需要更多面向分析的建模,而自动驾驶功能可能会使用针对机器学习算法优化的模型。
It’s safe to assume that the operation model, implementing the tickets’ lifecycle, will be different from the one used for fraud detection and the support autopilot feature. Fraud detection algorithms usually require more analytics-oriented modeling, whereas, the autopilot feature is likely to use a model optimized for use with machine learning algorithms.
D:分开的方式。该模式需要在多个有界上下文中重复实现功能。应该不惜一切代价避免复制复杂的、易变的和业务关键的业务逻辑。
D: Separate ways. The pattern entails duplicate implementation of a functionality in multiple bounded contexts. Duplicating complex, volatile, and business-critical business logic should be avoided at all costs.
A:核心子域。核心子域最有可能利用反腐败层来保护自己免受上游服务暴露的无效模型的影响,或者包含上游公共接口中的频繁更改。
A: Core subdomain. A core subdomain is most likely to leverage an anticorruption layer to protect itself from ineffective models exposed by upstream services, or to contain frequent changes in the upstream’s public interfaces.
A:核心子域。核心子域最有可能实现开放主机服务。将其实现模型与公共接口(已发布语言)解耦,可以更方便地发展核心子域的模型,而不会影响其下游消费者。
A: Core subdomain. A core subdomain is most likely to implement the open-host service. Decoupling its implementation model from the public interface (published language) makes it more convenient to evolve the core subdomain’s model without affecting its downstream consumers.
B:共享内核。共享内核模式是限界上下文的单一团队所有权规则的一个例外。它定义了共享模型的一小部分,并且可以由多个限界上下文同时演化。模型的共享部分应始终保持尽可能小。
B: Shared kernel. The shared kernel pattern is an exception to the bounded contexts’ single team ownership rule. It defines a small portion of the model that is shared and can be evolved simultaneously by multiple bounded contexts. The shared part of the model should be always kept as small as possible.
C:这些模式都不能用于实现核心子域。事务脚本和活动记录都适用于简单的业务逻辑,而核心子域涉及更复杂的业务逻辑。
C: Neither of these patterns can be used to implement a core subdomain. Both transaction script and active record lend themselves to the case of simple business logic, whereas core subdomains involve more complex business logic.
D:以上问题都有可能:
如果在第 6 行之后执行失败,调用者重试操作,并且该方法选择了相同的代理FindLeastBusyAgent,代理的 ActiveTickets 计数器将增加 1 以上。
如果在第 6 行之后执行失败但调用者没有重试该操作,则计数器将增加,而票证本身将不会被创建。
如果在第 12 行之后执行失败,则创建并分配工单,但不会发送第 14 行的通知。
D: All of the above issues are possible:
If the execution fails after line 6, the caller retries the operation, and the same agent is chosen by the FindLeastBusyAgent method, the agent’s ActiveTickets counter will be increased by more than 1.
If the execution fails after line 6 but the caller doesn’t retry the operation, the counter will be increased, while the ticket itself won’t be created.
If the execution fails after line 12, the ticket is created and assigned, but the notification on line 14 won’t be sent.
如果在第 12 行之后执行失败并且调用者重试操作并成功,则相同的票证将被持久化并分配两次。
If the execution fails after line 12 and the caller retries the operation and it succeeds, the same ticket will be persisted and assigned twice.
WolfDesk 的所有支持子域都是作为事务脚本或活动记录实现的良好候选者,因为它们的业务逻辑相对简单:
管理租户的门票类别
管理租户的产品,客户可以就这些产品开具支持票
输入租户支持代理的工作时间表
All of WolfDesk’s supporting subdomains are good candidates for implementation as transaction script or active record as their business logic is relatively straightforward:
Management of a tenant’s ticket categories
Management of a tenant’s products, regarding which the customers can open support tickets
Entry of a tenant’s support agents’ work schedules
C:值对象是不可变的。(此外,它们可以同时包含数据和行为。)
C: Value objects are immutable. (Also, they can contain both data and behavior.)
B:只要业务领域的数据一致性要求完好无损,聚合应该设计得尽可能小。
B: Aggregates should be designed to be as small as possible, as long as the business domain’s data consistency requirements are intact.
B:确保正确的事务边界。
B: To ensure correct transactional boundaries.
D:A和C。
D: A and C.
B:聚合封装了它的所有业务逻辑,但是操作活动记录的业务逻辑可以位于其边界之外。
B: An aggregate encapsulates all of its business logic, but business logic manipulating an active record can be located outside of its boundary.
A:领域事件使用值对象来描述业务领域中发生的事情。
A: Domain events use value objects to describe what has happened in the business domain.
C:可以投影多个状态表示,您以后可以随时添加其他投影。
C: Multiple state representations can be projected and you can always add additional projections in the future.
D:B 和 C 都正确。
D: Both B and C are correct.
票证生命周期算法是作为事件源域模型实现的一个很好的候选者。为所有状态转换生成域事件可以更方便地投影针对欺诈检测算法和支持自动驾驶功能优化的其他状态表示。
The ticket lifecycle algorithm is a good candidate to be implemented as an event-sourced domain model. Generating domain events for all state transitions can make it more convenient to project additional state representations optimized for the fraud detection algorithm and the support autopilot functionality.
D:A和C。
D: A and C.
D:B和C。
D: B and C.
C:基础设施层。
C: Infrastructure layer.
E:A 和 D。
E: A and D.
使用 CQRS 模式投射的多个模型并不与限界上下文作为模型边界的要求相矛盾,因为只有一个模型被定义为真实来源并用于更改聚合的状态。
Working with multiple models projected by the CQRS pattern doesn’t contradict the bounded context’s requirement of being a model boundary, since only one of the models is defined as the source of truth and is used for making changes in the aggregates’ states.
事件源域模型、CQRS 架构和侧重于单元测试的测试策略。
Event-sourced domain model, CQRS architecture, and testing strategy that focuses on unit tests.
可以将轮班建模为活动记录,以分层架构模式工作。测试策略应该主要关注集成测试。
The shifts can be modeled as active records, working in the layered architectural pattern. The testing strategy should primarily focus on integration tests.
业务逻辑可以作为事务脚本实现,以分层架构组织。从测试的角度来看,值得专注于端到端测试,验证完整的集成流程。
The business logic can be implemented as a transaction script, organized in a layered architecture. From a testing perspective, it’s worth concentrating on end-to-end tests, verifying the full integration flow.
答:与客户-供应商的合作伙伴关系(守规矩、反腐败层或开放主机服务)。随着组织的发展,团队以临时方式集成限界上下文变得更具挑战性。因此,他们转而使用更正式的集成模式。
A: Partnership to customer–supplier (conformist, anticorruption layer, or open-host service). As an organization grows, it can become more challenging for teams to integrate their bounded contexts in an ad hoc fashion. As a result, they switch to a more formal integration pattern.
D:A 和 B。A 是正确的,因为当复制成本低于协作开销时,限界上下文会分道扬镳。C 是不正确的,因为复制核心子域的实现是一个糟糕的主意。因此,B 是正确的,因为分离方式模式可用于支持和通用子域。
D: A and B. A is correct because bounded contexts go separate ways when the cost of duplication is lower than the overhead of collaboration. C is incorrect because it’s a terrible idea to duplicate implementation of a core subdomain. Consequently, B is correct because the separate ways pattern can be used for supporting and generic subdomains.
D:B和C。
D: B and C.
F:A和C。
F: A and C.
在达到一定的增长水平后,WolfDesk 可以跟随亚马逊的脚步并实施自己的计算平台,以进一步优化其弹性扩展的能力并优化其基础架构成本。
Upon reaching a certain level of growth, WolfDesk could follow the footsteps of Amazon and implement its own compute platform to further optimize its ability to scale elastically and optimize its infrastructure costs.
D:所有利益相关者都了解您想要探索的业务领域。
D: All stakeholders having knowledge of the business domain that you want to explore.
F:所有的答案都是促进 EventStorming 会话的充分理由。
F: All the answers are sound reasons to facilitate an EventStorming session.
E:所有答案都是 EventStorming 会话的可能结果。您应该期望获得的结果取决于您促进会议的最初目的。
E: All the answers are possible outcomes of an EventStorming session. The outcome you should expect to get depends on your initial purpose for facilitating the session.
B:分析组织的业务领域及其战略。
B: Analyze the organization’s business domain and its strategy.
D:A和B。
D: A and B.
C:A和B。
C: A and B.
具有有界上下文范围边界的聚合可以使所有有界上下文的数据成为一个大事务的一部分。这种方法的性能问题也可能从一开始就很明显。一旦发生这种情况,交易边界将被删除。因此,将不再可能假设聚合中的信息是高度一致的。
An aggregate with a bounded context-wide boundary may make all of the bounded context’s data a part of one big transaction. It’s also likely that performance issues with this approach will be evident from the get go. Once that happens, the transactional boundary will be removed. As a result, it will no longer be possible to assume that the information residing in the aggregate is strongly consistent.
A:所有的微服务都是限界上下文。(但并非所有限界上下文都是微服务。)
A: All microservices are bounded contexts. (But not all bounded contexts are microservices.)
D:业务领域的知识及其跨服务边界公开并由其公共接口反映的复杂性。
D: The knowledge of the business domain and its intricacies exposed across the service’s boundary and reflected by its public interface.
C:有界上下文(最宽)和微服务(最窄)之间的边界。
C: Boundaries between bounded contexts (widest) and microservices (narrowest).
D:决定取决于业务领域。
D: The decision depends on the business domain.
D:A 和 B 正确。
D: A and B are correct.
B:事件携带状态转移。
B: Event-carried state transfer.
A: 开放主机服务。
A: Open-host service.
B:S2 应该发布公共事件通知,通知 S1 发出同步请求以获取最新信息。
B: S2 should publish public event notifications, which will signal S1 to issue a synchronous request to get the most up-to-date information.
D:A 和 C 正确。
D: A and C are correct.
B:开放主机服务。开放主机服务公开的一种已发布语言可以是针对分析处理优化的 OLAP 数据。
B: Open-host service. One of the published languages exposed by the open-host service can be OLAP data optimized for analytical processing.
C:CQRS。可以利用 CQRS 模式从事务模型中生成 OLAP 模型的投影。
C: CQRS. The CQRS pattern can be leveraged to generate projections of the OLAP model out of the transactional model.
Brandolini, A. (nd)。介绍 EventStorming。精益酒吧。
Brandolini, A. (n.d.). Introducing EventStorming. Leanpub.
小布鲁克斯 FP (1974)。人月神话和其他关于软件工程的论文。马萨诸塞州雷丁:Addison-Wesley。
Brooks, F. P., Jr. (1974). The Mythical Man Month and Other Essays on Software Engineering. Reading, MA: Addison-Wesley.
Eisenhardt, K., & Sull, D. (2016)。简单的规则:如何在复杂的世界中取得成功。伦敦:约翰·默里。
Eisenhardt, K., & Sull, D. (2016). Simple Rules: How to Succeed in a Complex World. London: John Murray.
Esposito, D., & Saltarello, A. (2008)。为企业构建应用程序:Microsoft ® .NET . 华盛顿州雷德蒙德:微软出版社。
Esposito, D., & Saltarello, A. (2008). Architecting Applications for the Enterprise: Microsoft® .NET. Redmond, WA: Microsoft Press.
Evans, E. (2003)。领域驱动设计:解决软件核心的复杂性。波士顿:Addison-Wesley。
Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Boston: Addison-Wesley.
羽毛,MC (2005)。有效地使用遗留代码。新泽西州上马鞍河:Prentice Hall PTR。
Feathers, M. C. (2005). Working Effectively with Legacy Code. Upper Saddle River, NJ: Prentice Hall PTR.
福勒,M. (2002)。企业应用架构模式。波士顿:Addison-Wesley。
Fowler, M. (2002). Patterns of Enterprise Application Architecture. Boston: Addison-Wesley.
M. 福勒 (2019)。重构:改进现有代码的设计(第 2 版)。波士顿:Addison-Wesley。
Fowler, M. (2019). Refactoring: Improving the Design of Existing Code (2nd ed.). Boston: Addison-Wesley.
Fowler, M.(日期不详)。“事件驱动”是什么意思?2021 年 8 月 12 日从https://martinfowler.com/articles/201701-event-driven.html检索。
Fowler, M. (n.d.). What do you mean by “Event-Driven”? Retrieved August 12, 2021, from https://martinfowler.com/articles/201701-event-driven.html.
Gamma, E.、Helm, R. 和 Johnson, R. (1994)。设计模式:可重用面向对象软件的要素。马萨诸塞州雷丁:Addison-Wesley。
Gamma, E., Helm, R., & Johnson, R. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley.
Gigerenzer, G.、Todd, PM 和 ABC 研究组(研究组,马克斯普朗克研究所,德国)。(1999)。使我们聪明的简单启发法。纽约:牛津大学出版社。
Gigerenzer, G., Todd, P. M., & ABC Research Group (Research Group, Max Planck Institute, Germany). (1999). Simple Heuristics That Make Us Smart. New York: Oxford University Press.
高德拉特,EM(2005 年)。超越目标:约束理论。纽约:Gildan Audio。
Goldratt, E. M. (2005). Beyond the Goal: Theory of Constraints. New York: Gildan Audio.
Goldratt, EM 和 Goldratt-Ashlag, E. (2018)。选择。马萨诸塞州大巴灵顿:北河出版社出版公司。
Goldratt, E. M., & Goldratt-Ashlag, E. (2018). The Choice. Great Barrington, MA: North River Press Publishing Corporation.
Goldratt-Ashlag, E. (2010)。“阻力层——根据 TOC 的买入过程。” (约束理论手册第 20 章。)英国贝德福德:Goldratt Marketing Group。
Goldratt-Ashlag, E. (2010). “The Layers of Resistance—The Buy-In Process According to TOC.” (Chapter 20 of the Theory of Constraints handbook.) Bedford, England: Goldratt Marketing Group.
Garcia-Molina, H., & Salem K. (1987)。传奇。新泽西州普林斯顿:普林斯顿大学计算机科学系。
Garcia-Molina, H., & Salem K. (1987). Sagas. Princeton, NJ: Department of Computer Science, Princeton University.
Helland, P. (2020)。外部数据与内部数据。ACM 通讯,63 (11),111–118。
Helland, P. (2020). Data on the outside versus data on the inside. Communications of the ACM, 63(11), 111–118.
Hohpe, G., & Woolf, B. (2003)。企业集成模式:设计、构建和部署消息传递解决方案。波士顿:Addison-Wesley。
Hohpe, G., & Woolf, B. (2003). Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Boston: Addison-Wesley.
霍诺诺夫 V.(2022 年)。软件设计中的平衡耦合。波士顿:Addison-Wesley。
Khononov, V. (2022). Balancing Coupling in Software Design. Boston: Addison-Wesley.
霍诺诺夫 V.(2019 年)。什么是领域驱动设计?波士顿:奥莱利。
Khononov, V. (2019). What Is Domain-Driven Design? Boston: O’Reilly.
Martraire, C. (2019)。动态文档:通过设计持续共享知识。波士顿:Addison-Wesley。
Martraire, C. (2019). Living Documentation: Continuous Knowledge Sharing by Design. Boston: Addison-Wesley.
Millett, S., & Tune, N. (2015)。领域驱动设计的模式、原则和实践(第 1 版)。纳什维尔:John Wiley & Sons。
Millett, S., & Tune, N. (2015). Patterns, Principles, and Practices of Domain-Driven Design (1st ed.). Nashville: John Wiley & Sons.
迈尔斯,GJ(1978 年)。复合/结构化设计。纽约:Van Nostrand Reinhold。
Myers, G. J. (1978). Composite/Structured Design. New York: Van Nostrand Reinhold.
J. 奥斯特豪特 (2018)。软件设计哲学。加利福尼亚州帕洛阿尔托:Yaknyam 出版社。
Ousterhout, J. (2018). A Philosophy of Software Design. Palo Alto, CA: Yaknyam Press.
理查森 C.(2019 年)。微服务模式:带有 Java 示例。纽约:曼宁出版社。
Richardson, C. (2019). Microservice Patterns: With Examples in Java. New York: Manning Publications.
Vernon, V. (2013)。实施领域驱动设计。波士顿:Addison-Wesley。
Vernon, V. (2013). Implementing Domain-Driven Design. Boston: Addison-Wesley.
弗农 V.(2016 年)。领域驱动设计提炼。波士顿:Addison-Wesley。
Vernon, V. (2016). Domain-Driven Design Distilled. Boston: Addison-Wesley.
韦斯特 G.(2018 年)。规模:生物体、城市和公司的生死普遍法则。英国牛津:Weidenfeld & Nicolson。
West, G. (2018). Scale: The Universal Laws of Life and Death in Organisms, Cities and Companies. Oxford, England: Weidenfeld & Nicolson.
Wright, D., & Meadows, DH (2009)。系统思考:入门。伦敦:Earthscan。
Wright, D., & Meadows, D. H. (2009). Thinking in Systems: A Primer. London: Earthscan.
Learning Domain-Driven Design封面上的动物是蒙纳猴 ( Cercopithecus mona ),它们可以在西非和加勒比海岛屿的热带森林中找到,它们是在奴隶贸易期间被引进的。它们从树冠中部跳到顶部,用长长的尾巴保持平衡。
The animal on the cover of Learning Domain-Driven Design is a mona monkey (Cercopithecus mona), which can be found in the tropical forests of West Africa and the Caribbean islands, where they were introduced during the slave trade. They leap from trees in the mid- to top canopy, using their long tails for balance.
莫纳猴的脸、四肢和尾巴周围的毛发呈褐色,颜色较深。它们的下侧,包括腿的内侧,都是白色的。雌性平均长 16 英寸,而雄性平均长 20 英寸——尾巴又增加了 26 英寸或更多。莫纳猴脸颊上的长簇毛发呈黄色或灰色,它们的鼻子呈淡粉色。脸颊在它们觅食时充当食物袋,尽可能多地容纳它们的胃。
Mona monkeys have brownish fur that’s darker around their faces, limbs, and on their tails. Their undersides, including the insides of their legs, are white. Females average 16 inches in length while males average 20 inches—and the tails add another 26 inches or more. Long tufts of fur on the cheeks of Mona monkeys can tint yellow or gray, and their noses have some light pink coloring. The cheeks serve as pouches for food as they forage, holding as much as their stomachs can.
莫纳猴以水果、种子、昆虫和树叶为食,在野外生活了大约 30 年。每天,他们成群结队地觅食多次。超过 40 个的包装已被记录在案;通常情况下,雄性在群体中占主导地位,与多只雌性交配并击退竞争的雄性。这些群体会变得非常嘈杂。
Mona monkeys eat fruit, seeds, insects, and leaves and live for about 30 years in the wild. Each day, they forage multiple times in large groups. Packs larger than 40 have been documented; typically a male dominates the group, mating with multiple females and fighting off competing males. These groups can get very noisy.
由于人类活动,莫纳猴的保护状况为近危。O'Reilly 封面上的许多动物都濒临灭绝;所有这些对世界都很重要。
Mona monkeys have a conservation status of Near Threatened due to human activities. Many of the animals on O’Reilly covers are endangered; all of them are important to the world.
封面插图由 Karen Montgomery 绘制,基于Lydekker 的皇家自然历史的黑白版画。封面字体是 Gilroy Semibold 和 Guardian Sans。文字字体为Adobe Minion Pro;标题字体为 Adobe Myriad Condensed;代码字体是 Dalton Maag 的 Ubuntu Mono。
The cover illustration is by Karen Montgomery, based on a black and white engraving from Lydekker’s Royal Natural History. The cover fonts are Gilroy Semibold and Guardian Sans. The text font is Adobe Minion Pro; the heading font is Adobe Myriad Condensed; and the code font is Dalton Maag’s Ubuntu Mono.